├── .claude └── commands │ ├── analyze-code.md │ ├── lsmcp-onboarding.md │ ├── reduce-similarities.md │ └── refactor.md ├── .githooks └── pre-commit ├── .github ├── FUNDING.yaml ├── renovate.json └── workflows │ ├── check-pr-title.yaml │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .lsmcp ├── config.json └── memories │ ├── symbol_index_info.md │ └── symbol_index_status.md ├── .mcp.json ├── .npmrc ├── CLAUDE.md ├── LICENSE ├── README.md ├── bun.lock ├── ccusage.example.json ├── config-schema.json ├── docs ├── .gitignore ├── .vitepress │ └── config.ts ├── guide │ ├── blocks-reports.md │ ├── cli-options.md │ ├── config-files.md │ ├── configuration.md │ ├── cost-modes.md │ ├── custom-paths.md │ ├── daily-reports.md │ ├── directory-detection.md │ ├── environment-variables.md │ ├── getting-started.md │ ├── index.md │ ├── installation.md │ ├── json-output.md │ ├── library-usage.md │ ├── live-monitoring.md │ ├── mcp-server.md │ ├── monthly-reports.md │ ├── related-projects.md │ ├── session-reports.md │ ├── sponsors.md │ ├── statusline.md │ └── weekly-reports.md ├── index.md ├── package.json ├── public │ ├── blocks-live.png │ ├── ccusage_thumbnail.png │ ├── favicon.svg │ ├── logo.png │ ├── logo.svg │ ├── mcp-claude-desktop.avif │ └── screenshot.png ├── tsconfig.json ├── typedoc.config.mjs ├── update-api-index.ts └── wrangler.jsonc ├── eslint.config.js ├── package.json ├── scripts └── generate-json-schema.ts ├── src ├── _config-loader-tokens.ts ├── _consts.ts ├── _daily-grouping.ts ├── _date-utils.ts ├── _jq-processor.ts ├── _json-output-types.ts ├── _live-monitor.ts ├── _live-rendering.ts ├── _macro.ts ├── _project-names.ts ├── _session-blocks.ts ├── _shared-args.ts ├── _table.ts ├── _terminal-utils.ts ├── _token-utils.ts ├── _types.ts ├── _utils.ts ├── calculate-cost.ts ├── commands │ ├── _blocks.live.ts │ ├── _session_id.ts │ ├── blocks.ts │ ├── daily.ts │ ├── index.ts │ ├── mcp.ts │ ├── monthly.ts │ ├── session.ts │ ├── statusline.ts │ └── weekly.ts ├── data-loader.ts ├── debug.ts ├── index.ts ├── logger.ts ├── mcp.ts └── pricing-fetcher.ts ├── test ├── statusline-test-opus4.json ├── statusline-test-sonnet4.json ├── statusline-test-sonnet41.json ├── statusline-test.json └── test-transcript.jsonl ├── tsconfig.json ├── tsdown.config.ts ├── typos.toml └── vitest.config.ts /.claude/commands/analyze-code.md: -------------------------------------------------------------------------------- 1 | # Code Analysis with LSMCP MCP 2 | 3 | This command uses the LSMCP (Language Server MCP) tools to perform rapid semantic code analysis, leveraging language server capabilities for more accurate results than traditional grep/search approaches. 4 | 5 | ## Overview 6 | 7 | LSMCP provides semantic code analysis through language server protocol, enabling: 8 | 9 | - Type-aware error detection 10 | - Symbol reference tracking 11 | - Cross-file dependency analysis 12 | - Real-time diagnostics 13 | 14 | ## Command Template 15 | 16 | ```markdown 17 | # Rapid Code Analysis Task 18 | 19 | Project Root: 20 | Target Language: 21 | Analysis Goal: 22 | 23 | Execute the following analysis phases: 24 | 25 | ## Phase 1: Capability Check (5 seconds) 26 | 27 | Verify available LSP features: 28 | ``` 29 | 30 | mcp**lsmcp-**check_capabilities 31 | 32 | ``` 33 | 34 | ## Phase 2: Project-wide Diagnostics (10 seconds) 35 | Collect all errors and warnings: 36 | ``` 37 | 38 | mcp**lsmcp-**get_all_diagnostics 39 | 40 | - root: "" 41 | - pattern: "\*_/_." 42 | - severityFilter: "error" # Start with errors only 43 | - useGitignore: true 44 | 45 | ``` 46 | 47 | If few errors found, repeat with severityFilter: "warning" 48 | 49 | ## Phase 3: Symbol Analysis (5 seconds per symbol) 50 | For critical functions/classes: 51 | ``` 52 | 53 | mcp**lsmcp-**find_references 54 | 55 | - root: "" 56 | - filePath: "" 57 | - line: 58 | - symbolName: "" 59 | 60 | ``` 61 | 62 | ## Phase 4: Type Verification (3 seconds per location) 63 | For suspicious code locations: 64 | ``` 65 | 66 | mcp**lsmcp-**get_hover 67 | 68 | - root: "" 69 | - filePath: "" 70 | - line: 71 | - target: "" 72 | 73 | ``` 74 | 75 | ## Phase 5: Unused Code Detection 76 | Identify symbols with zero references: 77 | ``` 78 | 79 | mcp**lsmcp-**find_references 80 | 81 | - Check exported functions/classes 82 | - Flag items with 0 references 83 | 84 | ``` 85 | 86 | ## Report Format 87 | 1. Executive Summary 88 | - Total errors/warnings count 89 | - Critical issues requiring immediate attention 90 | 91 | 2. Detailed Findings 92 | - Group by severity (Error > Warning > Info) 93 | - Include file:line references 94 | - Show affected symbol names 95 | 96 | 3. Impact Analysis 97 | - Number of files affected 98 | - Cross-module dependencies 99 | 100 | 4. Recommendations 101 | - Priority fixes 102 | - Refactoring suggestions 103 | - Code quality improvements 104 | ``` 105 | 106 | ## Supported Language Adapters 107 | 108 | | Language | Adapter Name | File Extension | Special Notes | 109 | | --------------------- | ------------ | -------------------- | -------------------------------- | 110 | | TypeScript/JavaScript | tsgo-dev | .ts, .tsx, .js, .jsx | Limited rename/workspace symbols | 111 | | Python | python-dev | .py | Full LSP support | 112 | | Rust | rust-dev | .rs | Requires rust-analyzer | 113 | | Go | go-dev | .go | Requires gopls | 114 | | F# | fsharp-dev | .fs, .fsx | Requires fsautocomplete | 115 | 116 | ## Example Usage 117 | 118 | ### TypeScript Project Analysis 119 | 120 | ```markdown 121 | Project Root: /home/user/my-ts-project 122 | Target Language: typescript 123 | Analysis Goal: Find type errors and unused exports 124 | 125 | ## Phase 1: Capability Check 126 | 127 | mcp**lsmcp-tsgo-dev**check_capabilities 128 | 129 | ## Phase 2: Project-wide Diagnostics 130 | 131 | mcp**lsmcp-tsgo-dev**get_all_diagnostics 132 | 133 | - root: "/home/user/my-ts-project" 134 | - pattern: "src/\*_/_.ts" 135 | - severityFilter: "error" 136 | ``` 137 | 138 | ### Python Code Quality Check 139 | 140 | ```markdown 141 | Project Root: /home/user/my-python-app 142 | Target Language: python 143 | Analysis Goal: Identify undefined variables and import issues 144 | 145 | ## Phase 1: Capability Check 146 | 147 | mcp**lsmcp-python-dev**check_capabilities 148 | 149 | ## Phase 2: Project-wide Diagnostics 150 | 151 | mcp**lsmcp-python-dev**get_all_diagnostics 152 | 153 | - root: "/home/user/my-python-app" 154 | - pattern: "\*_/_.py" 155 | - severityFilter: "error" 156 | ``` 157 | 158 | ## Performance Expectations 159 | 160 | - **Phase 1**: Instant (<1s) 161 | - **Phase 2**: 5-30s depending on project size 162 | - **Phase 3**: 2-5s per symbol 163 | - **Phase 4**: 1-3s per hover request 164 | - **Phase 5**: 10-60s for full project scan 165 | 166 | ## Advantages over Traditional Search 167 | 168 | 1. **Semantic Accuracy**: Understands language constructs, not just text patterns 169 | 2. **Type Awareness**: Catches type mismatches and incompatibilities 170 | 3. **Cross-file Intelligence**: Tracks references across module boundaries 171 | 4. **Real-time Feedback**: Uses pre-indexed LSP data for instant results 172 | 173 | ## Limitations 174 | 175 | - Requires language server support for the target language 176 | - Some features may be limited by specific language server implementations 177 | - Initial indexing may take time for large projects 178 | - Memory usage scales with project size 179 | 180 | ## High-Level Analysis Tools 181 | 182 | LSMCP now includes high-level analysis tools that provide faster symbol search and indexing capabilities: 183 | 184 | ### Symbol Indexing Tools 185 | 186 | 1. **index_files** - Build or update the symbol index 187 | 188 | ``` 189 | mcp__lsmcp-__index_files 190 | - pattern: "**/*.ts" 191 | - root: "/project/path" 192 | - concurrency: 5 193 | ``` 194 | 195 | 2. **find_symbol** - Fast symbol search using the index 196 | 197 | ``` 198 | mcp__lsmcp-__find_symbol 199 | - name: "MyClass" # Optional: partial matching supported 200 | - kind: ["Class", "Interface"] # Optional: filter by symbol types 201 | - file: "src/models.ts" # Optional: search within specific file 202 | - includeChildren: true # Include nested symbols 203 | ``` 204 | 205 | 3. **get_file_symbols** - Get all symbols in a file from index 206 | 207 | ``` 208 | mcp__lsmcp-__get_file_symbols 209 | - filePath: "src/components/Button.tsx" 210 | ``` 211 | 212 | 4. **get_index_stats** - View index statistics 213 | 214 | ``` 215 | mcp__lsmcp-__get_index_stats 216 | ``` 217 | 218 | 5. **clear_index** - Clear the symbol index 219 | ``` 220 | mcp__lsmcp-__clear_index 221 | ``` 222 | 223 | ### Example: Fast Symbol Search Workflow 224 | 225 | ```markdown 226 | # Step 1: Build the index 227 | 228 | mcp**lsmcp-tsgo-dev**index_files 229 | 230 | - pattern: "src/\*_/_.ts" 231 | - concurrency: 10 232 | 233 | # Step 2: Find all classes and interfaces 234 | 235 | mcp**lsmcp-tsgo-dev**find_symbol 236 | 237 | - kind: ["Class", "Interface"] 238 | 239 | # Step 3: Search for specific symbol 240 | 241 | mcp**lsmcp-tsgo-dev**find_symbol 242 | 243 | - name: "User" 244 | - includeChildren: true 245 | 246 | # Step 4: Get detailed symbols for a file 247 | 248 | mcp**lsmcp-tsgo-dev**get_file_symbols 249 | 250 | - filePath: "src/models/User.ts" 251 | ``` 252 | 253 | ### Performance Benefits 254 | 255 | - **Initial indexing**: 5-30 seconds for large projects 256 | - **Symbol queries**: <100ms (vs 1-5 seconds with LSP) 257 | - **File updates**: Automatic reindexing on changes 258 | - **Memory efficient**: Indexed data structure 259 | 260 | ## Tips for Effective Analysis 261 | 262 | 1. Start with error-level diagnostics before checking warnings 263 | 2. Focus on high-traffic symbols (frequently imported/used) 264 | 3. Use specific file patterns to narrow scope when needed 265 | 4. Combine with traditional grep for text-pattern searches 266 | 5. Run analysis after significant refactoring to catch regressions 267 | 6. Build symbol index once at the start for faster repeated queries 268 | 7. Use partial name matching for flexible symbol search 269 | -------------------------------------------------------------------------------- /.claude/commands/lsmcp-onboarding.md: -------------------------------------------------------------------------------- 1 | start lsmcp onboarding 2 | -------------------------------------------------------------------------------- /.claude/commands/reduce-similarities.md: -------------------------------------------------------------------------------- 1 | Run `similarity-ts .` to detect semantic code similarities. Execute this command, analyze the duplicate code patterns, and create a refactoring plan. Check `similarity-ts -h` for detailed options. 2 | -------------------------------------------------------------------------------- /.claude/commands/refactor.md: -------------------------------------------------------------------------------- 1 | # Refactor Command 2 | 3 | When I need to refactor TypeScript code in this project, follow these steps: 4 | 5 | ## 1. Analyze Current Code Structure 6 | 7 | - Use `mcp_lsmcp__get_module_symbols` to understand module exports 8 | - Use `mcp_lsmcp__get_type_at_symbol` to analyze type information 9 | - Use `mcp_lsmcp__find_references` to find all usages before refactoring 10 | 11 | ## 2. Choose Appropriate Refactoring Tools 12 | 13 | ### For Symbol Operations 14 | 15 | - **Rename**: Use `mcp_lsmcp__rename_symbol` instead of Edit/MultiEdit 16 | - **Delete**: Use `mcp_lsmcp__delete_symbol` to remove symbols and their references 17 | - **Find usages**: Use `mcp_lsmcp__find_references` instead of grep 18 | 19 | ### For File Operations 20 | 21 | - **Move file**: Use `mcp_lsmcp__move_file` instead of bash mv 22 | - **Move directory**: Use `mcp_lsmcp__move_directory` for entire directories 23 | 24 | ### For Type Analysis 25 | 26 | - **Get type info**: Use `mcp_lsmcp__get_type_at_symbol` 27 | - **Module analysis**: Use `mcp_lsmcp__get_module_symbols` 28 | - **Scope analysis**: Use `mcp_lsmcp__get_symbols_in_scope` 29 | 30 | ## 3. Verify Changes 31 | 32 | After refactoring: 33 | 34 | 1. Run `pnpm typecheck` to ensure no TypeScript errors 35 | 2. Run `pnpm lint` to check code style 36 | 3. Run `pnpm test` to verify functionality 37 | 4. Use `mcp_lsmcp__get_diagnostics` to check for issues 38 | 39 | ## 4. Common Refactoring Patterns 40 | 41 | ### Extract Interface/Type 42 | 43 | 1. Identify the type structure using `get_type_at_symbol` 44 | 2. Create new type definition file 45 | 3. Use `rename_symbol` to update references 46 | 47 | ### Move Module 48 | 49 | 1. Use `move_file` or `move_directory` 50 | 2. Tool automatically updates all imports 51 | 3. Verify with `get_diagnostics` 52 | 53 | ### Rename Across Project 54 | 55 | 1. Use `find_references` to see impact 56 | 2. Use `rename_symbol` for safe renaming 57 | 3. Check affected files with `git status` 58 | 59 | ## 5. Best Practices 60 | 61 | - Always use TypeScript MCP tools for semantic operations 62 | - Never use text-based Edit/MultiEdit for refactoring 63 | - Check references before deleting or moving 64 | - Run tests after each refactoring step 65 | - Commit frequently with descriptive messages 66 | 67 | ## Example Commands 68 | 69 | ```bash 70 | # Rename a function across the project 71 | > Use mcp_lsmcp__rename_symbol to rename processData to transformData in src/utils.ts 72 | 73 | # Move a module to a new location 74 | > Use mcp_lsmcp__move_file to move src/helpers/parser.ts to src/core/parser.ts 75 | 76 | # Find all usages before refactoring 77 | > Use mcp_lsmcp__find_references for the Parser class in src/parser.ts 78 | ``` 79 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run lint-staged 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: ryoppippi 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>ryoppippi/renovate-config:no-group" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/check-pr-title.yaml: -------------------------------------------------------------------------------- 1 | name: Check PR title 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - edited 9 | - synchronize 10 | 11 | permissions: 12 | pull-requests: read 13 | 14 | jobs: 15 | main: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: amannn/action-semantic-pull-request@7f33ba792281b034f64e96f4c0b5496782dd3b37 # v6.1.0 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | ignoreLabels: | 24 | autorelease: pending 25 | dependencies 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint-check: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 13 | - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 14 | with: 15 | bun-version: latest 16 | - run: bun install --frozen-lockfile 17 | - run: bun lint 18 | - run: bun typecheck 19 | 20 | test: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 25 | - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 26 | with: 27 | bun-version: latest 28 | - run: bun install --frozen-lockfile 29 | - name: Check jq is available 30 | run: which jq && jq --version 31 | - name: Create default Claude directories for tests 32 | run: | 33 | mkdir -p $HOME/.claude/projects 34 | mkdir -p $HOME/.config/claude/projects 35 | - run: bun run test 36 | 37 | npm-publish-dry-run-and-upload-pkg-pr-now: 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 42 | - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 43 | with: 44 | bun-version: latest 45 | - run: bun install --frozen-lockfile 46 | - run: bunx pkg-pr-new publish 47 | 48 | spell-check: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout Actions Repository 52 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 53 | 54 | - uses: crate-ci/typos@master 55 | with: 56 | config: ./typos.toml 57 | 58 | schema-check: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 62 | - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 63 | with: 64 | bun-version: latest 65 | - run: bun install --frozen-lockfile 66 | - name: Generate schema files 67 | run: bun run generate:schema 68 | - name: Check if schema files are up-to-date 69 | run: | 70 | if git diff --exit-code config-schema.json docs/public/config-schema.json; then 71 | echo "✅ Schema files are up-to-date" 72 | else 73 | echo "❌ Schema files are not up-to-date. Please run 'bun run generate:schema' and commit the changes." 74 | echo "" 75 | echo "Changed files:" 76 | git diff --name-only config-schema.json docs/public/config-schema.json 77 | echo "" 78 | echo "Diff:" 79 | git diff config-schema.json docs/public/config-schema.json 80 | exit 1 81 | fi 82 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | npm: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 10 12 | permissions: 13 | contents: read 14 | id-token: write 15 | steps: 16 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 20 | with: 21 | registry-url: 'https://registry.npmjs.org' 22 | node-version: lts/* 23 | - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 24 | - run: npm install -g npm@latest 25 | - run: bun install --frozen-lockfile 26 | - run: npm publish --provenance --no-git-checks --access public 27 | working-directory: ${{env.PACKAGE_DIR}} 28 | 29 | release: 30 | needs: 31 | - npm 32 | runs-on: ubuntu-latest 33 | permissions: 34 | contents: write 35 | steps: 36 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 37 | with: 38 | fetch-depth: 0 39 | - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 40 | with: 41 | bun-version: latest 42 | - run: bun x changelogithub 43 | env: 44 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | 36 | .eslintcache 37 | 38 | # lsmcp cache 39 | .lsmcp/cache 40 | -------------------------------------------------------------------------------- /.lsmcp/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "tsgo" 3 | } 4 | -------------------------------------------------------------------------------- /.lsmcp/memories/symbol_index_info.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: 2025-08-10T20:53:29.066Z 3 | updated: 2025-08-22T11:06:14.669Z 4 | --- 5 | 6 | # Symbol Index Configuration for ccusage 7 | 8 | ## Project Setup 9 | - **Language**: TypeScript 10 | - **Pattern Used**: `src/**/*.ts` (automatically indexed by project overview) 11 | - **Root Directory**: `/Users/ryoppippi/ghq/github.com/ryoppippi/ccusage` 12 | 13 | ## Index Statistics 14 | - **Total Files**: 37 15 | - **Total Symbols**: 226 16 | - **Indexing Time**: 3s 17 | - **Last Updated**: 2025-08-22T11:05:39.166Z 18 | 19 | ## Symbol Breakdown 20 | - **Classes**: 61 21 | - **Interfaces**: 0 22 | - **Functions**: 102 23 | - **Methods**: 31 24 | - **Properties**: 28 25 | 26 | ## Key Features Verified 27 | ✅ Symbol search by name (e.g., 'main' finds 2 functions) 28 | ✅ Symbol search by kind (e.g., 'Class' finds 61 classes) 29 | ✅ LSP integration working (definitions, references, hover) 30 | ✅ Project structure automatically detected 31 | 32 | ## Common Search Patterns 33 | - Search by name: `search_symbols --name "functionName"` 34 | - Search by type: `search_symbols --kind "Class"` 35 | - Search in specific file: `search_symbols --file "src/commands/"` 36 | - Get file symbols: `lsp_get_document_symbols --relativePath "src/index.ts"` 37 | 38 | ## Issues Encountered 39 | None - indexing completed successfully on first attempt. 40 | 41 | ## Architecture Notes 42 | This is a CLI tool for Claude Code usage analysis with clean TypeScript structure: 43 | - Main entry: `src/index.ts` 44 | - Commands: `src/commands/` 45 | - Internal utilities: `src/_*.ts` (underscore prefix) 46 | - Data types: `src/_types.ts` 47 | - Configuration: TypeScript with strict mode enabled -------------------------------------------------------------------------------- /.lsmcp/memories/symbol_index_status.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: 2025-08-15T10:09:44.271Z 3 | updated: 2025-08-15T11:07:13.578Z 4 | --- 5 | 6 | Symbol Index Onboarding Complete - ccusage Project 7 | 8 | Project: /Users/ryoppippi/ghq/github.com/ryoppippi/ccusage 9 | Language: TypeScript/JavaScript 10 | Pattern Used: \*_/_.{ts,tsx,js,jsx} (automatic detection) 11 | 12 | INDEX STATISTICS: 13 | 14 | - Total files indexed: 34 15 | - Total symbols: 209 16 | - Indexing time: 1.44 seconds 17 | - Average time per file: 41ms 18 | 19 | KEY SYMBOLS FOUND: 20 | 21 | - 87 Functions (including main, formatRemainingTime, calculateCost, etc.) 22 | - Core command handlers in src/commands/ 23 | - Utility functions in src/\_\*.ts files 24 | - Type definitions and schemas 25 | 26 | RECENT ADDITIONS (Feature: Dynamic Context Limits): 27 | 28 | - `getModelContextLimit` method in PricingFetcher class 29 | - Enhanced `calculateContextTokens` function with model-specific limits 30 | - Updated statusline command to support dynamic context calculations 31 | - Added context limit fields to modelPricingSchema (max_tokens, max_input_tokens, max_output_tokens) 32 | - Comprehensive test coverage for new functionality 33 | - Test fixtures for Claude 4 model variants (Sonnet 4, Opus 4.1, Sonnet 4.1) 34 | 35 | STATUS: ✅ Fully operational 36 | 37 | - Symbol search working correctly 38 | - Fast lookup capabilities enabled 39 | - Ready for advanced code navigation 40 | - Latest dynamic context limit functionality indexed 41 | 42 | RECOMMENDED USAGE: 43 | 44 | - Use search_symbol_from_index for fast symbol lookup 45 | - Use get_definitions to navigate to symbol definitions 46 | - Use find_references to trace symbol usage 47 | - Leverage kind filtering (Function, Class, Interface, etc.) 48 | 49 | TESTING COMMANDS: 50 | 51 | - `bun run test:statusline:sonnet4` - Test with Claude 4 Sonnet 52 | - `bun run test:statusline:opus4` - Test with Claude 4.1 Opus 53 | - `bun run test:statusline:sonnet41` - Test with Claude 4.1 Sonnet 54 | - `bun run test:statusline:all` - Run all model tests 55 | 56 | Last updated: 2025-08-15 (Dynamic Context Limits feature) 57 | -------------------------------------------------------------------------------- /.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "context7": { 4 | "type": "http", 5 | "url": "https://mcp.context7.com/mcp" 6 | }, 7 | "grep": { 8 | "type": "http", 9 | "url": "https://mcp.grep.app" 10 | }, 11 | "lsmcp": { 12 | "command": "bun", 13 | "args": [ 14 | "lsmcp", 15 | "-p", 16 | "tsgo" 17 | ] 18 | }, 19 | "@praha/byethrow": { 20 | "type": "stdio", 21 | "command": "bun", 22 | "args": [ 23 | "byethrow-mcp" 24 | ], 25 | "env": {} 26 | }, 27 | "gunshi-doc": { 28 | "command": "bun", 29 | "args": [ 30 | "x", 31 | "sitemcp", 32 | "https://gunshi.dev/", 33 | "--concurrency 10", 34 | "--no-cache" 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @jsr:registry=https://npm.jsr.io 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ryoppippi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ccusage.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./config-schema.json", 3 | "defaults": { 4 | "json": true, 5 | "mode": "auto", 6 | "timezone": "Asia/Tokyo", 7 | "locale": "ja-JP", 8 | "offline": false, 9 | "breakdown": false 10 | }, 11 | "commands": { 12 | "daily": { 13 | "instances": true, 14 | "order": "desc", 15 | "projectAliases": "ccusage=Usage Tracker,my-long-project-name=Project X" 16 | }, 17 | "monthly": { 18 | "breakdown": true 19 | }, 20 | "weekly": { 21 | "startOfWeek": "monday" 22 | }, 23 | "session": { 24 | "order": "desc" 25 | }, 26 | "blocks": { 27 | "tokenLimit": "500000", 28 | "sessionLength": 5, 29 | "active": false 30 | }, 31 | "statusline": { 32 | "offline": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # VitePress build output 2 | .vitepress/dist/ 3 | .vitepress/cache/ 4 | 5 | # Generated documentation 6 | api/ 7 | 8 | # Dependencies 9 | node_modules/ 10 | 11 | # Temporary files 12 | .temp/ 13 | .cache/ 14 | 15 | # OS files 16 | .DS_Store 17 | Thumbs.db 18 | 19 | public/_redirects 20 | public/config-schema.json 21 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | import * as path from 'node:path'; 3 | import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons'; 4 | import llmstxt from 'vitepress-plugin-llms'; 5 | import { withMermaid } from 'vitepress-plugin-mermaid'; 6 | import typedocSidebar from '../api/typedoc-sidebar.json'; 7 | import { cloudflareRedirect } from '@ryoppippi/vite-plugin-cloudflare-redirect' 8 | 9 | export default withMermaid(defineConfig({ 10 | title: 'ccusage', 11 | description: 'Usage analysis tool for Claude Code', 12 | base: '/', 13 | cleanUrls: true, 14 | ignoreDeadLinks: true, 15 | 16 | head: [ 17 | ['link', { rel: 'icon', href: '/favicon.svg' }], 18 | ['meta', { name: 'theme-color', content: '#646cff' }], 19 | ['meta', { property: 'og:type', content: 'website' }], 20 | ['meta', { property: 'og:locale', content: 'en' }], 21 | ['meta', { property: 'og:title', content: 'ccusage | Claude Code Usage Analysis' }], 22 | ['meta', { property: 'og:site_name', content: 'ccusage' }], 23 | ['meta', { property: 'og:image', content: 'https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/logo.png' }], 24 | ['meta', { property: 'og:url', content: 'https://github.com/ryoppippi/ccusage' }], 25 | ], 26 | 27 | themeConfig: { 28 | logo: '/logo.svg', 29 | 30 | nav: [ 31 | { text: 'Guide', link: '/guide/' }, 32 | { text: 'API Reference', link: '/api/' }, 33 | { 34 | text: 'Links', 35 | items: [ 36 | { text: 'GitHub', link: 'https://github.com/ryoppippi/ccusage' }, 37 | { text: 'npm', link: 'https://www.npmjs.com/package/ccusage' }, 38 | { text: 'Changelog', link: 'https://github.com/ryoppippi/ccusage/releases' }, 39 | { text: 'DeepWiki', link: 'https://deepwiki.com/ryoppippi/ccusage' }, 40 | { text: 'Package Stats', link: 'https://tanstack.com/ccusage?npmPackage=ccusage' }, 41 | ], 42 | }, 43 | ], 44 | 45 | sidebar: { 46 | '/guide/': [ 47 | { 48 | text: 'Introduction', 49 | items: [ 50 | { text: 'Introduction', link: '/guide/' }, 51 | { text: 'Getting Started', link: '/guide/getting-started' }, 52 | { text: 'Installation', link: '/guide/installation' }, 53 | ], 54 | }, 55 | { 56 | text: 'Usage', 57 | items: [ 58 | { text: 'Daily Reports', link: '/guide/daily-reports' }, 59 | { text: 'Weekly Reports', link: '/guide/weekly-reports' }, 60 | { text: 'Monthly Reports', link: '/guide/monthly-reports' }, 61 | { text: 'Session Reports', link: '/guide/session-reports' }, 62 | { text: 'Blocks Reports', link: '/guide/blocks-reports' }, 63 | { text: 'Live Monitoring', link: '/guide/live-monitoring' }, 64 | ], 65 | }, 66 | { 67 | text: 'Configuration', 68 | items: [ 69 | { text: 'Overview', link: '/guide/configuration' }, 70 | { text: 'Command-Line Options', link: '/guide/cli-options' }, 71 | { text: 'Environment Variables', link: '/guide/environment-variables' }, 72 | { text: 'Configuration Files', link: '/guide/config-files' }, 73 | { text: 'Directory Detection', link: '/guide/directory-detection' }, 74 | { text: 'Custom Paths', link: '/guide/custom-paths' }, 75 | { text: 'Cost Calculation Modes', link: '/guide/cost-modes' }, 76 | ], 77 | }, 78 | { 79 | text: 'Integration', 80 | items: [ 81 | { text: 'Library Usage', link: '/guide/library-usage' }, 82 | { text: 'MCP Server', link: '/guide/mcp-server' }, 83 | { text: 'JSON Output', link: '/guide/json-output' }, 84 | { text: 'Statusline Integration', link: '/guide/statusline' }, 85 | ], 86 | }, 87 | { 88 | text: 'Community', 89 | items: [ 90 | { text: 'Related Projects', link: '/guide/related-projects' }, 91 | { text: 'Sponsors', link: '/guide/sponsors' }, 92 | ], 93 | }, 94 | ], 95 | '/api/': [ 96 | { 97 | text: 'API Reference', 98 | items: [ 99 | { text: 'Overview', link: '/api/' }, 100 | ...typedocSidebar, 101 | ], 102 | }, 103 | ], 104 | }, 105 | 106 | socialLinks: [ 107 | { icon: 'github', link: 'https://github.com/ryoppippi/ccusage' }, 108 | { icon: 'npm', link: 'https://www.npmjs.com/package/ccusage' }, 109 | ], 110 | 111 | footer: { 112 | message: 'Released under the MIT License.', 113 | copyright: 'Copyright © 2024 ryoppippi', 114 | }, 115 | 116 | search: { 117 | provider: 'local', 118 | }, 119 | 120 | editLink: { 121 | pattern: 'https://github.com/ryoppippi/ccusage/edit/main/docs/:path', 122 | text: 'Edit this page on GitHub', 123 | }, 124 | 125 | lastUpdated: { 126 | text: 'Updated at', 127 | formatOptions: { 128 | year: 'numeric', 129 | month: '2-digit', 130 | day: '2-digit', 131 | hour: '2-digit', 132 | minute: '2-digit', 133 | hour12: false, 134 | timeZone: 'UTC', 135 | }, 136 | }, 137 | }, 138 | 139 | vite: { 140 | plugins: [ 141 | cloudflareRedirect({ 142 | mode: "generate", 143 | entries: [ 144 | { from: '/raycast', to: 'https://www.raycast.com/nyatinte/ccusage', status: 302 }, 145 | { from: '/gh', to: 'https://github.com/ryoppippi/ccusage', status: 302 }, 146 | { from: '/npm', to: 'https://www.npmjs.com/package/ccusage', status: 302 }, 147 | { from: '/deepwiki', to: 'https://deepwiki.com/ryoppippi/ccusage', status: 302 }, 148 | ] 149 | }), 150 | groupIconVitePlugin(), 151 | ...llmstxt(), 152 | ], 153 | }, 154 | 155 | markdown: { 156 | config(md) { 157 | md.use(groupIconMdPlugin); 158 | }, 159 | }, 160 | mermaid: { 161 | // Optional mermaid configuration 162 | }, 163 | })); 164 | -------------------------------------------------------------------------------- /docs/guide/cli-options.md: -------------------------------------------------------------------------------- 1 | # Command-Line Options 2 | 3 | ccusage provides extensive command-line options to customize its behavior. These options take precedence over configuration files and environment variables. 4 | 5 | ## Global Options 6 | 7 | All ccusage commands support these global options: 8 | 9 | ### Date Filtering 10 | 11 | Filter usage data by date range: 12 | 13 | ```bash 14 | # Filter by date range 15 | ccusage daily --since 20250101 --until 20250630 16 | 17 | # Show data from a specific date 18 | ccusage monthly --since 20250101 19 | 20 | # Show data up to a specific date 21 | ccusage session --until 20250630 22 | ``` 23 | 24 | ### Output Format 25 | 26 | Control how data is displayed: 27 | 28 | ```bash 29 | # JSON output for programmatic use 30 | ccusage daily --json 31 | ccusage daily -j 32 | 33 | # Show per-model breakdown 34 | ccusage daily --breakdown 35 | ccusage daily -b 36 | 37 | # Combine options 38 | ccusage daily --json --breakdown 39 | ``` 40 | 41 | ### Cost Calculation Mode 42 | 43 | Choose how costs are calculated: 44 | 45 | ```bash 46 | # Auto mode (default) - use costUSD when available 47 | ccusage daily --mode auto 48 | 49 | # Calculate mode - always calculate from tokens 50 | ccusage daily --mode calculate 51 | 52 | # Display mode - only show pre-calculated costUSD 53 | ccusage daily --mode display 54 | ``` 55 | 56 | ### Sort Order 57 | 58 | Control the ordering of results: 59 | 60 | ```bash 61 | # Newest first (default) 62 | ccusage daily --order desc 63 | 64 | # Oldest first 65 | ccusage daily --order asc 66 | ``` 67 | 68 | ### Offline Mode 69 | 70 | Run without network connectivity: 71 | 72 | ```bash 73 | # Use cached pricing data 74 | ccusage daily --offline 75 | ccusage daily -O 76 | ``` 77 | 78 | ### Timezone 79 | 80 | Set the timezone for date calculations: 81 | 82 | ```bash 83 | # Use UTC timezone 84 | ccusage daily --timezone UTC 85 | 86 | # Use specific timezone 87 | ccusage daily --timezone America/New_York 88 | ccusage daily -z Asia/Tokyo 89 | 90 | # Short alias 91 | ccusage monthly -z Europe/London 92 | ``` 93 | 94 | #### Timezone Effect 95 | 96 | The timezone affects how usage is grouped by date. For example, usage at 11 PM UTC on January 1st would appear on: 97 | - **January 1st** when `--timezone UTC` 98 | - **January 1st** when `--timezone America/New_York` (6 PM EST) 99 | - **January 2nd** when `--timezone Asia/Tokyo` (8 AM JST next day) 100 | 101 | ### Locale 102 | 103 | Control date and time formatting: 104 | 105 | ```bash 106 | # US English (12-hour time format) 107 | ccusage daily --locale en-US 108 | 109 | # Japanese (24-hour time format) 110 | ccusage blocks --locale ja-JP 111 | 112 | # German (24-hour time format) 113 | ccusage session -l de-DE 114 | 115 | # Short alias 116 | ccusage daily -l fr-FR 117 | ``` 118 | 119 | #### Locale Effects 120 | 121 | The locale affects display formatting: 122 | 123 | **Date Format:** 124 | - `en-US`: 08/04/2025 125 | - `en-CA`: 2025-08-04 (ISO format, default) 126 | - `ja-JP`: 2025/08/04 127 | - `de-DE`: 04.08.2025 128 | 129 | **Time Format:** 130 | - `en-US`: 3:30:00 PM (12-hour) 131 | - Others: 15:30:00 (24-hour) 132 | 133 | ### Debug Options 134 | 135 | Get detailed debugging information: 136 | 137 | ```bash 138 | # Debug mode - show pricing mismatches and config loading 139 | ccusage daily --debug 140 | 141 | # Show sample discrepancies 142 | ccusage daily --debug --debug-samples 10 143 | ``` 144 | 145 | ### Configuration File 146 | 147 | Use a custom configuration file: 148 | 149 | ```bash 150 | # Specify custom config file 151 | ccusage daily --config ./my-config.json 152 | ccusage monthly --config /path/to/team-config.json 153 | ``` 154 | 155 | ## Command-Specific Options 156 | 157 | ### Daily Command 158 | 159 | Additional options for daily reports: 160 | 161 | ```bash 162 | # Group by project 163 | ccusage daily --instances 164 | ccusage daily -i 165 | 166 | # Filter to specific project 167 | ccusage daily --project myproject 168 | ccusage daily -p myproject 169 | 170 | # Combine project filtering 171 | ccusage daily --instances --project myproject 172 | ``` 173 | 174 | ### Weekly Command 175 | 176 | Options for weekly reports: 177 | 178 | ```bash 179 | # Set week start day 180 | ccusage weekly --start-of-week monday 181 | ccusage weekly --start-of-week sunday 182 | ``` 183 | 184 | ### Session Command 185 | 186 | Options for session reports: 187 | 188 | ```bash 189 | # Filter by session ID 190 | ccusage session --id abc123-session 191 | 192 | # Filter by project 193 | ccusage session --project myproject 194 | ``` 195 | 196 | ### Blocks Command 197 | 198 | Options for 5-hour billing blocks: 199 | 200 | ```bash 201 | # Show only active block 202 | ccusage blocks --active 203 | ccusage blocks -a 204 | 205 | # Show recent blocks (last 3 days) 206 | ccusage blocks --recent 207 | ccusage blocks -r 208 | 209 | # Set token limit for warnings 210 | ccusage blocks --token-limit 500000 211 | ccusage blocks --token-limit max 212 | 213 | # Live monitoring mode 214 | ccusage blocks --live 215 | ccusage blocks --live --refresh-interval 2 216 | 217 | # Customize session length 218 | ccusage blocks --session-length 5 219 | ``` 220 | 221 | ### MCP Server 222 | 223 | Options for MCP server: 224 | 225 | ```bash 226 | # Default stdio transport 227 | ccusage mcp 228 | 229 | # HTTP transport 230 | ccusage mcp --type http --port 8080 231 | 232 | # Custom cost mode 233 | ccusage mcp --mode calculate 234 | ``` 235 | 236 | ### Statusline 237 | 238 | Options for statusline display: 239 | 240 | ```bash 241 | # Basic statusline 242 | ccusage statusline 243 | 244 | # Force offline mode 245 | ccusage statusline --offline 246 | 247 | # Enable caching 248 | ccusage statusline --cache 249 | 250 | # Custom refresh interval 251 | ccusage statusline --refresh-interval 5 252 | ``` 253 | 254 | ## JSON Output Options 255 | 256 | When using `--json` output, additional processing options are available: 257 | 258 | ```bash 259 | # Apply jq filter to JSON output 260 | ccusage daily --json --jq ".data[]" 261 | 262 | # Filter high-cost days 263 | ccusage daily --json --jq ".data[] | select(.cost > 10)" 264 | 265 | # Extract specific fields 266 | ccusage session --json --jq ".data[] | {date, cost}" 267 | ``` 268 | 269 | ## Option Precedence 270 | 271 | Options are applied in this order (highest to lowest priority): 272 | 273 | 1. **Command-line arguments** - Direct CLI options 274 | 2. **Custom config file** - Via `--config` flag 275 | 3. **Local project config** - `.ccusage/ccusage.json` 276 | 4. **User config** - `~/.config/claude/ccusage.json` 277 | 5. **Legacy config** - `~/.claude/ccusage.json` 278 | 6. **Built-in defaults** 279 | 280 | ## Examples 281 | 282 | ### Development Workflow 283 | 284 | ```bash 285 | # Daily development check 286 | ccusage daily --instances --breakdown 287 | 288 | # Check specific project costs 289 | ccusage daily --project myapp --since 20250101 290 | 291 | # Export for reporting 292 | ccusage monthly --json > monthly-report.json 293 | ``` 294 | 295 | ### Team Collaboration 296 | 297 | ```bash 298 | # Use team configuration 299 | ccusage daily --config ./team-config.json 300 | 301 | # Consistent timezone for remote team 302 | ccusage daily --timezone UTC --locale en-CA 303 | 304 | # Generate shareable report 305 | ccusage weekly --json --jq ".summary" 306 | ``` 307 | 308 | ### Cost Monitoring 309 | 310 | ```bash 311 | # Monitor active usage 312 | ccusage blocks --active --live 313 | 314 | # Check if approaching limits 315 | ccusage blocks --token-limit 500000 316 | 317 | # Historical analysis 318 | ccusage monthly --mode calculate --breakdown 319 | ``` 320 | 321 | ### Debugging Issues 322 | 323 | ```bash 324 | # Debug configuration loading 325 | ccusage daily --debug --config ./test-config.json 326 | 327 | # Check pricing discrepancies 328 | ccusage daily --debug --debug-samples 20 329 | 330 | # Silent mode for scripts 331 | LOG_LEVEL=0 ccusage daily --json 332 | ``` 333 | 334 | ## Short Aliases 335 | 336 | Many options have short aliases for convenience: 337 | 338 | | Long Option | Short | Description | 339 | |------------|-------|-------------| 340 | | `--json` | `-j` | JSON output | 341 | | `--breakdown` | `-b` | Per-model breakdown | 342 | | `--offline` | `-O` | Offline mode | 343 | | `--timezone` | `-z` | Set timezone | 344 | | `--locale` | `-l` | Set locale | 345 | | `--instances` | `-i` | Group by project | 346 | | `--project` | `-p` | Filter project | 347 | | `--active` | `-a` | Active block only | 348 | | `--recent` | `-r` | Recent blocks | 349 | 350 | ## Related Documentation 351 | 352 | - [Environment Variables](/guide/environment-variables) - Configure via environment 353 | - [Configuration Files](/guide/config-files) - Persistent configuration 354 | - [Cost Calculation Modes](/guide/cost-modes) - Understanding cost modes -------------------------------------------------------------------------------- /docs/guide/daily-reports.md: -------------------------------------------------------------------------------- 1 | # Daily Reports 2 | 3 | ![Daily usage report showing token usage and costs by date with model breakdown](/screenshot.png) 4 | 5 | Daily reports show token usage and costs aggregated by calendar date, giving you a clear view of your Claude Code usage patterns over time. 6 | 7 | ## Basic Usage 8 | 9 | Show all daily usage: 10 | 11 | ```bash 12 | ccusage daily 13 | # or simply: 14 | ccusage 15 | ``` 16 | 17 | The daily command is the default, so you can omit it when running ccusage. 18 | 19 | ## Example Output 20 | 21 | ![Daily usage report showing token usage and costs by date with model breakdown](/screenshot.png) 22 | 23 | ## Understanding the Columns 24 | 25 | ### Basic Columns 26 | 27 | - **Date**: Calendar date in YYYY-MM-DD format 28 | - **Models**: Claude models used that day (shown as bulleted list) 29 | - **Input**: Total input tokens sent to Claude 30 | - **Output**: Total output tokens received from Claude 31 | - **Cost (USD)**: Estimated cost for that day 32 | 33 | ### Cache Columns 34 | 35 | - **Cache Create**: Tokens used to create cache entries 36 | - **Cache Read**: Tokens read from cache (typically cheaper) 37 | 38 | ### Responsive Display 39 | 40 | ccusage automatically adapts to your terminal width: 41 | 42 | - **Wide terminals (≥100 chars)**: Shows all columns 43 | - **Narrow terminals (<100 chars)**: Compact mode with essential columns only 44 | 45 | ## Command Options 46 | 47 | ### Date Filtering 48 | 49 | Filter reports by date range: 50 | 51 | ```bash 52 | # Show usage from December 2024 53 | ccusage daily --since 20241201 --until 20241231 54 | 55 | # Show last week 56 | ccusage daily --since 20241215 --until 20241222 57 | 58 | # Show usage since a specific date 59 | ccusage daily --since 20241201 60 | ``` 61 | 62 | ### Sort Order 63 | 64 | Control the order of dates: 65 | 66 | ```bash 67 | # Newest dates first (default) 68 | ccusage daily --order desc 69 | 70 | # Oldest dates first 71 | ccusage daily --order asc 72 | ``` 73 | 74 | ### Cost Calculation Modes 75 | 76 | Control how costs are calculated: 77 | 78 | ```bash 79 | # Use pre-calculated costs when available (default) 80 | ccusage daily --mode auto 81 | 82 | # Always calculate costs from tokens 83 | ccusage daily --mode calculate 84 | 85 | # Only show pre-calculated costs 86 | ccusage daily --mode display 87 | ``` 88 | 89 | ### Model Breakdown 90 | 91 | See per-model cost breakdown: 92 | 93 | ```bash 94 | ccusage daily --breakdown 95 | ``` 96 | 97 | This shows costs split by individual models: 98 | 99 | ``` 100 | ┌──────────────┬──────────────────┬────────┬─────────┬────────────┐ 101 | │ Date │ Models │ Input │ Output │ Cost (USD) │ 102 | ├──────────────┼──────────────────┼────────┼─────────┼────────────┤ 103 | │ 2025-06-21 │ opus-4, sonnet-4 │ 277 │ 31,456 │ $17.58 │ 104 | ├──────────────┼──────────────────┼────────┼─────────┼────────────┤ 105 | │ └─ opus-4 │ │ 100 │ 15,000 │ $10.25 │ 106 | ├──────────────┼──────────────────┼────────┼─────────┼────────────┤ 107 | │ └─ sonnet-4│ │ 177 │ 16,456 │ $7.33 │ 108 | └──────────────┴──────────────────┴────────┴─────────┴────────────┘ 109 | ``` 110 | 111 | ### JSON Output 112 | 113 | Export data as JSON for further analysis: 114 | 115 | ```bash 116 | ccusage daily --json 117 | ``` 118 | 119 | ```json 120 | { 121 | "type": "daily", 122 | "data": [ 123 | { 124 | "date": "2025-06-21", 125 | "models": ["claude-opus-4-20250514", "claude-sonnet-4-20250514"], 126 | "inputTokens": 277, 127 | "outputTokens": 31456, 128 | "cacheCreationTokens": 512, 129 | "cacheReadTokens": 1024, 130 | "totalTokens": 33269, 131 | "costUSD": 17.58 132 | } 133 | ], 134 | "summary": { 135 | "totalInputTokens": 277, 136 | "totalOutputTokens": 31456, 137 | "totalCacheCreationTokens": 512, 138 | "totalCacheReadTokens": 1024, 139 | "totalTokens": 33269, 140 | "totalCostUSD": 17.58 141 | } 142 | } 143 | ``` 144 | 145 | ### Offline Mode 146 | 147 | Use cached pricing data without network access: 148 | 149 | ```bash 150 | ccusage daily --offline 151 | # or short form: 152 | ccusage daily -O 153 | ``` 154 | 155 | ### Project Analysis 156 | 157 | Group usage by project instead of aggregating across all projects: 158 | 159 | ```bash 160 | # Group daily usage by project 161 | ccusage daily --instances 162 | ccusage daily -i 163 | ``` 164 | 165 | When using `--instances`, the report shows usage for each project separately: 166 | 167 | ``` 168 | ┌──────────────┬────────────────────────────────────────────────────────────────────────────────────────────┐ 169 | │ Project: my-project │ 170 | ├──────────────┬──────────────────┬────────┬─────────┬────────────┬────────────┬─────────────┬──────────┤ 171 | │ Date │ Models │ Input │ Output │ Cache Create│ Cache Read │ Total Tokens│ Cost (USD)│ 172 | ├──────────────┼──────────────────┼────────┼─────────┼────────────┼────────────┼─────────────┼──────────┤ 173 | │ 2025-06-21 │ • sonnet-4 │ 277 │ 31,456 │ 512│ 1,024 │ 33,269 │ $7.33│ 174 | └──────────────┴──────────────────┴────────┴─────────┴────────────┴────────────┴─────────────┴──────────┘ 175 | 176 | ┌──────────────┬────────────────────────────────────────────────────────────────────────────────────────────┐ 177 | │ Project: other-project │ 178 | ├──────────────┬──────────────────┬────────┬─────────┬────────────┬────────────┬─────────────┬──────────┤ 179 | │ Date │ Models │ Input │ Output │ Cache Create│ Cache Read │ Total Tokens│ Cost (USD)│ 180 | ├──────────────┼──────────────────┼────────┼─────────┼────────────┼────────────┼─────────────┼──────────┤ 181 | │ 2025-06-21 │ • opus-4 │ 100 │ 15,000 │ 256│ 512 │ 15,868 │ $10.25│ 182 | └──────────────┴──────────────────┴────────┴─────────┴────────────┴────────────┴─────────────┴──────────┘ 183 | ``` 184 | 185 | Filter to a specific project: 186 | 187 | ```bash 188 | # Show only usage from "my-project" 189 | ccusage daily --project my-project 190 | ccusage daily -p my-project 191 | 192 | # Combine with instances flag 193 | ccusage daily --instances --project my-project 194 | ``` 195 | 196 | ## Common Use Cases 197 | 198 | ### Track Monthly Spending 199 | 200 | ```bash 201 | # See December 2024 usage 202 | ccusage daily --since 20241201 --until 20241231 203 | ``` 204 | 205 | ### Find Expensive Days 206 | 207 | ```bash 208 | # Sort by cost (highest first) 209 | ccusage daily --order desc 210 | ``` 211 | 212 | ### Export for Spreadsheet Analysis 213 | 214 | ```bash 215 | ccusage daily --json > december-usage.json 216 | ``` 217 | 218 | ### Compare Model Usage 219 | 220 | ```bash 221 | # See which models you use most 222 | ccusage daily --breakdown 223 | ``` 224 | 225 | ### Check Recent Activity 226 | 227 | ```bash 228 | # Last 7 days 229 | ccusage daily --since $(date -d '7 days ago' +%Y%m%d) 230 | ``` 231 | 232 | ### Analyze Project Usage 233 | 234 | ```bash 235 | # See usage breakdown by project 236 | ccusage daily --instances 237 | 238 | # Track specific project costs 239 | ccusage daily --project my-important-project --since 20250601 240 | 241 | # Compare project usage with JSON export 242 | ccusage daily --instances --json > project-analysis.json 243 | ``` 244 | 245 | ### Team Usage Analysis 246 | 247 | Use project aliases to replace cryptic or long project directory names with readable labels: 248 | 249 | ```json 250 | // .ccusage/ccusage.json - Set custom project names for better reporting 251 | { 252 | "commands": { 253 | "daily": { 254 | "projectAliases": "uuid-project=Frontend App,long-name=Backend API" 255 | } 256 | } 257 | } 258 | ``` 259 | 260 | The `projectAliases` setting uses a comma-separated format of `original-name=display-name` pairs. This is especially useful when: 261 | - Your projects have UUID-based names (e.g., `a2cd99ed-a586=My App`) 262 | - Directory names are long paths that get truncated 263 | - You want consistent naming across team reports 264 | 265 | ```bash 266 | # Generate team report with readable project names 267 | ccusage daily --instances --since 20250601 268 | # Now shows "Frontend App" instead of "uuid-project" 269 | ``` 270 | 271 | ## Tips 272 | 273 | 1. **Compact Mode**: If your terminal is narrow, expand it to see all columns 274 | 2. **Date Format**: Use YYYYMMDD format for date filters (e.g., 20241225) 275 | 3. **Regular Monitoring**: Run daily reports regularly to track usage patterns 276 | 4. **JSON Export**: Use `--json` for creating charts or additional analysis 277 | 278 | ## Related Commands 279 | 280 | - [Monthly Reports](/guide/monthly-reports) - Aggregate by month 281 | - [Session Reports](/guide/session-reports) - Per-conversation analysis 282 | - [Blocks Reports](/guide/blocks-reports) - 5-hour billing windows 283 | - [Live Monitoring](/guide/live-monitoring) - Real-time tracking 284 | -------------------------------------------------------------------------------- /docs/guide/directory-detection.md: -------------------------------------------------------------------------------- 1 | # Directory Detection 2 | 3 | ccusage automatically detects and manages Claude Code data directories. 4 | 5 | ## Default Directory Locations 6 | 7 | ccusage automatically searches for Claude Code data in these locations: 8 | 9 | - **`~/.config/claude/projects/`** - New default location (Claude Code v1.0.30+) 10 | - **`~/.claude/projects/`** - Legacy location (pre-v1.0.30) 11 | 12 | When no custom directory is specified, ccusage searches both locations and aggregates data from all valid directories found. 13 | 14 | ::: info Breaking Change 15 | The directory change from `~/.claude` to `~/.config/claude` in Claude Code v1.0.30 was an undocumented breaking change. ccusage handles both locations automatically to ensure backward compatibility. 16 | ::: 17 | 18 | ## Search Priority 19 | 20 | When `CLAUDE_CONFIG_DIR` environment variable is not set, ccusage searches in this order: 21 | 22 | 1. **Primary**: `~/.config/claude/projects/` (preferred for newer installations) 23 | 2. **Fallback**: `~/.claude/projects/` (for legacy installations) 24 | 25 | Data from all valid directories is automatically combined. 26 | 27 | ## Custom Directory Configuration 28 | 29 | ### Single Custom Directory 30 | 31 | Override the default search with a specific directory: 32 | 33 | ```bash 34 | export CLAUDE_CONFIG_DIR="/custom/path/to/claude" 35 | ccusage daily 36 | ``` 37 | 38 | ### Multiple Directories 39 | 40 | Aggregate data from multiple Claude installations: 41 | 42 | ```bash 43 | export CLAUDE_CONFIG_DIR="/path/to/claude1,/path/to/claude2" 44 | ccusage daily 45 | ``` 46 | 47 | ## Directory Structure 48 | 49 | Claude Code stores usage data in a specific structure: 50 | 51 | ``` 52 | ~/.config/claude/projects/ 53 | ├── project-name-1/ 54 | │ ├── session-id-1.jsonl 55 | │ ├── session-id-2.jsonl 56 | │ └── session-id-3.jsonl 57 | ├── project-name-2/ 58 | │ └── session-id-4.jsonl 59 | └── project-name-3/ 60 | └── session-id-5.jsonl 61 | ``` 62 | 63 | Each: 64 | - **Project directory** represents a different Claude Code project/workspace 65 | - **JSONL file** contains usage data for a specific session 66 | - **Session ID** in the filename matches the `sessionId` field within the file 67 | 68 | ## Troubleshooting 69 | 70 | ### No Data Found 71 | 72 | If ccusage reports no data found: 73 | 74 | ```bash 75 | # Check if directories exist 76 | ls -la ~/.claude/projects/ 77 | ls -la ~/.config/claude/projects/ 78 | 79 | # Verify environment variable 80 | echo $CLAUDE_CONFIG_DIR 81 | 82 | # Test with explicit directory 83 | export CLAUDE_CONFIG_DIR="/path/to/claude" 84 | ccusage daily 85 | ``` 86 | 87 | ### Permission Errors 88 | 89 | ```bash 90 | # Check directory permissions 91 | ls -la ~/.claude/ 92 | ls -la ~/.config/claude/ 93 | 94 | # Fix permissions if needed 95 | chmod -R 755 ~/.claude/ 96 | chmod -R 755 ~/.config/claude/ 97 | ``` 98 | 99 | ### Wrong Directory Detection 100 | 101 | ```bash 102 | # Force specific directory 103 | export CLAUDE_CONFIG_DIR="/exact/path/to/claude" 104 | ccusage daily 105 | 106 | # Verify which directory is being used 107 | LOG_LEVEL=4 ccusage daily 108 | ``` 109 | 110 | ## Related Documentation 111 | 112 | - [Environment Variables](/guide/environment-variables) - Configure with CLAUDE_CONFIG_DIR 113 | - [Custom Paths](/guide/custom-paths) - Advanced path management 114 | - [Configuration Overview](/guide/configuration) - Complete configuration guide -------------------------------------------------------------------------------- /docs/guide/environment-variables.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | ccusage supports several environment variables for configuration and customization. Environment variables provide a way to configure ccusage without modifying command-line arguments or configuration files. 4 | 5 | ## CLAUDE_CONFIG_DIR 6 | 7 | Specifies where ccusage should look for Claude Code data. This is the most important environment variable for ccusage. 8 | 9 | ### Single Directory 10 | 11 | Set a single custom Claude data directory: 12 | 13 | ```bash 14 | export CLAUDE_CONFIG_DIR="/path/to/your/claude/data" 15 | ccusage daily 16 | ``` 17 | 18 | ### Multiple Directories 19 | 20 | Set multiple directories (comma-separated) to aggregate data from multiple sources: 21 | 22 | ```bash 23 | export CLAUDE_CONFIG_DIR="/path/to/claude1,/path/to/claude2" 24 | ccusage daily 25 | ``` 26 | 27 | When multiple directories are specified, ccusage automatically aggregates usage data from all valid locations. 28 | 29 | ### Default Behavior 30 | 31 | When `CLAUDE_CONFIG_DIR` is not set, ccusage automatically searches in: 32 | 1. `~/.config/claude/projects/` (new default, Claude Code v1.0.30+) 33 | 2. `~/.claude/projects/` (legacy location, pre-v1.0.30) 34 | 35 | Data from all valid directories is automatically combined. 36 | 37 | ::: info Directory Change 38 | The directory change from `~/.claude` to `~/.config/claude` in Claude Code v1.0.30 was an undocumented breaking change. ccusage handles both locations automatically for backward compatibility. 39 | ::: 40 | 41 | ### Use Cases 42 | 43 | #### Development Environment 44 | 45 | ```bash 46 | # Set in your shell profile (.bashrc, .zshrc, config.fish) 47 | export CLAUDE_CONFIG_DIR="$HOME/.config/claude" 48 | ``` 49 | 50 | #### Multiple Claude Installations 51 | 52 | ```bash 53 | # Aggregate data from different Claude installations 54 | export CLAUDE_CONFIG_DIR="$HOME/.claude,$HOME/.config/claude" 55 | ``` 56 | 57 | #### Team Shared Directory 58 | 59 | ```bash 60 | # Use team-shared data directory 61 | export CLAUDE_CONFIG_DIR="/team-shared/claude-data/$USER" 62 | ``` 63 | 64 | #### CI/CD Environment 65 | 66 | ```bash 67 | # Use specific directory in CI pipeline 68 | export CLAUDE_CONFIG_DIR="/ci-data/claude-logs" 69 | ccusage daily --json > usage-report.json 70 | ``` 71 | 72 | ## LOG_LEVEL 73 | 74 | Controls the verbosity of log output. ccusage uses [consola](https://github.com/unjs/consola) for logging under the hood. 75 | 76 | ### Log Levels 77 | 78 | | Level | Value | Description | Use Case | 79 | |-------|-------|-------------|----------| 80 | | Silent | `0` | Errors only | Scripts, piping output | 81 | | Warn | `1` | Warnings and errors | CI/CD environments | 82 | | Log | `2` | Normal logs | General use | 83 | | Info | `3` | Informational logs (default) | Standard operation | 84 | | Debug | `4` | Debug information | Troubleshooting | 85 | | Trace | `5` | All operations | Deep debugging | 86 | 87 | ### Usage Examples 88 | 89 | ```bash 90 | # Silent mode - only show results 91 | LOG_LEVEL=0 ccusage daily 92 | 93 | # Warning level - for CI/CD 94 | LOG_LEVEL=1 ccusage monthly 95 | 96 | # Debug mode - troubleshooting 97 | LOG_LEVEL=4 ccusage session 98 | 99 | # Trace everything - deep debugging 100 | LOG_LEVEL=5 ccusage blocks 101 | ``` 102 | 103 | ### Practical Applications 104 | 105 | #### Clean Output for Scripts 106 | 107 | ```bash 108 | # Get clean JSON output without logs 109 | LOG_LEVEL=0 ccusage daily --json | jq '.summary.totalCost' 110 | ``` 111 | 112 | #### CI/CD Pipeline 113 | 114 | ```bash 115 | # Show only warnings and errors in CI 116 | LOG_LEVEL=1 ccusage daily --instances 117 | ``` 118 | 119 | #### Debugging Issues 120 | 121 | ```bash 122 | # Maximum verbosity for troubleshooting 123 | LOG_LEVEL=5 ccusage daily --debug 124 | ``` 125 | 126 | #### Piping Output 127 | 128 | ```bash 129 | # Silent logs when piping to other commands 130 | LOG_LEVEL=0 ccusage monthly --json | python analyze.py 131 | ``` 132 | 133 | 134 | ## Additional Environment Variables 135 | 136 | ### CCUSAGE_OFFLINE 137 | 138 | Force offline mode by default: 139 | 140 | ```bash 141 | export CCUSAGE_OFFLINE=1 142 | ccusage daily # Runs in offline mode 143 | ``` 144 | 145 | ### NO_COLOR 146 | 147 | Disable colored output (standard CLI convention): 148 | 149 | ```bash 150 | export NO_COLOR=1 151 | ccusage daily # No color formatting 152 | ``` 153 | 154 | ### FORCE_COLOR 155 | 156 | Force colored output even when piping: 157 | 158 | ```bash 159 | export FORCE_COLOR=1 160 | ccusage daily | less -R # Preserves colors 161 | ``` 162 | 163 | ## Setting Environment Variables 164 | 165 | ### Temporary (Current Session) 166 | 167 | ```bash 168 | # Set for single command 169 | LOG_LEVEL=0 ccusage daily 170 | 171 | # Set for current shell session 172 | export CLAUDE_CONFIG_DIR="/custom/path" 173 | ccusage daily 174 | ``` 175 | 176 | ### Permanent (Shell Profile) 177 | 178 | Add to your shell configuration file: 179 | 180 | #### Bash (~/.bashrc) 181 | 182 | ```bash 183 | export CLAUDE_CONFIG_DIR="$HOME/.config/claude" 184 | export LOG_LEVEL=3 185 | ``` 186 | 187 | #### Zsh (~/.zshrc) 188 | 189 | ```zsh 190 | export CLAUDE_CONFIG_DIR="$HOME/.config/claude" 191 | export LOG_LEVEL=3 192 | ``` 193 | 194 | #### Fish (~/.config/fish/config.fish) 195 | 196 | ```fish 197 | set -x CLAUDE_CONFIG_DIR "$HOME/.config/claude" 198 | set -x LOG_LEVEL 3 199 | ``` 200 | 201 | #### PowerShell (Profile.ps1) 202 | 203 | ```powershell 204 | $env:CLAUDE_CONFIG_DIR = "$env:USERPROFILE\.config\claude" 205 | $env:LOG_LEVEL = "3" 206 | ``` 207 | 208 | ## Precedence 209 | 210 | Environment variables have lower precedence than command-line arguments but higher than configuration files: 211 | 212 | 1. **Command-line arguments** (highest priority) 213 | 2. **Environment variables** 214 | 3. **Configuration files** 215 | 4. **Built-in defaults** (lowest priority) 216 | 217 | Example: 218 | ```bash 219 | # Environment variable sets offline mode 220 | export CCUSAGE_OFFLINE=1 221 | 222 | # But command-line argument overrides it 223 | ccusage daily --no-offline # Runs in online mode 224 | ``` 225 | 226 | ## Debugging 227 | 228 | To see which environment variables are being used: 229 | 230 | ```bash 231 | # Show all environment variables 232 | env | grep -E "CLAUDE|CCUSAGE|LOG_LEVEL" 233 | 234 | # Debug mode shows environment variable usage 235 | LOG_LEVEL=4 ccusage daily --debug 236 | ``` 237 | 238 | ## Related Documentation 239 | 240 | - [Command-Line Options](/guide/cli-options) - CLI arguments and flags 241 | - [Configuration Files](/guide/config-files) - JSON configuration files 242 | - [Configuration Overview](/guide/configuration) - Complete configuration guide -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Welcome to ccusage! This guide will help you get up and running with analyzing your Claude Code usage data. 4 | 5 | ## Prerequisites 6 | 7 | - Claude Code installed and used (generates JSONL files) 8 | - Node.js 20+ or Bun runtime 9 | 10 | ## Quick Start 11 | 12 | The fastest way to try ccusage is to run it directly without installation: 13 | 14 | ::: code-group 15 | 16 | ```bash [npx] 17 | npx ccusage@latest 18 | ``` 19 | 20 | ```bash [bunx] 21 | bunx ccusage 22 | ``` 23 | 24 | ```bash [pnpm] 25 | pnpm dlx ccusage 26 | ``` 27 | 28 | ::: 29 | 30 | This will show your daily usage report by default. 31 | 32 | ## Your First Report 33 | 34 | When you run ccusage for the first time, you'll see a table showing your Claude Code usage by date: 35 | 36 | ``` 37 | ╭──────────────────────────────────────────╮ 38 | │ │ 39 | │ Claude Code Token Usage Report - Daily │ 40 | │ │ 41 | ╰──────────────────────────────────────────╯ 42 | 43 | ┌──────────────┬──────────────────┬────────┬─────────┬────────────┐ 44 | │ Date │ Models │ Input │ Output │ Cost (USD) │ 45 | ├──────────────┼──────────────────┼────────┼─────────┼────────────┤ 46 | │ 2025-06-21 │ • sonnet-4 │ 1,234 │ 15,678 │ $12.34 │ 47 | │ 2025-06-20 │ • opus-4 │ 890 │ 12,345 │ $18.92 │ 48 | └──────────────┴──────────────────┴────────┴─────────┴────────────┘ 49 | ``` 50 | 51 | ## Understanding the Output 52 | 53 | ### Columns Explained 54 | 55 | - **Date**: The date when Claude Code was used 56 | - **Models**: Which Claude models were used (Sonnet, Opus, etc.) 57 | - **Input**: Number of input tokens sent to Claude 58 | - **Output**: Number of output tokens received from Claude 59 | - **Cost (USD)**: Estimated cost based on model pricing 60 | 61 | ### Cache Tokens 62 | 63 | If you have a wide terminal, you'll also see cache token columns: 64 | 65 | - **Cache Create**: Tokens used to create cache entries 66 | - **Cache Read**: Tokens read from cache (typically cheaper) 67 | 68 | ## Next Steps 69 | 70 | Now that you have your first report, explore these features: 71 | 72 | 1. **[Weekly Reports](/guide/weekly-reports)** - Track usage patterns by week 73 | 2. **[Monthly Reports](/guide/monthly-reports)** - See usage aggregated by month 74 | 3. **[Session Reports](/guide/session-reports)** - Analyze individual conversations 75 | 4. **[Live Monitoring](/guide/live-monitoring)** - Real-time usage tracking 76 | 5. **[Configuration](/guide/configuration)** - Customize ccusage behavior 77 | 78 | ## Common Use Cases 79 | 80 | ### Monitor Daily Usage 81 | 82 | ```bash 83 | ccusage daily --since 20241201 --until 20241231 84 | ``` 85 | 86 | ### Find Expensive Sessions 87 | 88 | ```bash 89 | ccusage session --order desc 90 | ``` 91 | 92 | ### Export for Analysis 93 | 94 | ```bash 95 | ccusage monthly --json > usage-data.json 96 | ``` 97 | 98 | ### Live Session Monitoring 99 | 100 | ```bash 101 | ccusage blocks --live 102 | ``` 103 | 104 | ## Colors 105 | 106 | ccusage automatically colors the output based on the terminal's capabilities. If you want to disable colors, you can use the `--no-color` flag. Or you can use the `--color` flag to force colors on. 107 | 108 | ## Automatic Table Adjustment 109 | 110 | ccusage automatically adjusts its table layout based on terminal width: 111 | 112 | - **Wide terminals (≥100 characters)**: Full table with all columns including cache metrics, model names, and detailed breakdowns 113 | - **Narrow terminals (<100 characters)**: Compact view with essential columns only (Date, Models, Input, Output, Cost) 114 | 115 | The layout adjusts automatically based on your terminal width - no configuration needed. If you're in compact mode and want to see the full data, simply expand your terminal window. 116 | 117 | ## Troubleshooting 118 | 119 | ### No Data Found 120 | 121 | If ccusage shows no data, check: 122 | 123 | 1. **Claude Code is installed and used** - ccusage reads from Claude Code's data files 124 | 2. **Data directory exists** - Default locations: 125 | - `~/.config/claude/projects/` (new default) 126 | - `~/.claude/projects/` (legacy) 127 | 128 | ### Custom Data Directory 129 | 130 | If your Claude data is in a custom location: 131 | 132 | ```bash 133 | export CLAUDE_CONFIG_DIR="/path/to/your/claude/data" 134 | ccusage daily 135 | ``` 136 | 137 | ## Getting Help 138 | 139 | - Use `ccusage --help` for command options 140 | - Visit our [GitHub repository](https://github.com/ryoppippi/ccusage) for issues 141 | - Check the [API Reference](/api/) for programmatic usage 142 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ![ccusage daily report showing token usage and costs by date](/screenshot.png) 4 | 5 | **ccusage** (claude-code-usage) is a powerful CLI tool that analyzes your Claude Code usage from local JSONL files to help you understand your token consumption patterns and estimated costs. 6 | 7 | ## The Problem 8 | 9 | Claude Code's Max plan offers unlimited usage, which is fantastic! But many users are curious: 10 | 11 | - How much am I actually using Claude Code? 12 | - Which conversations are the most expensive? 13 | - What would I be paying on a pay-per-use plan? 14 | - Am I getting good value from my subscription? 15 | 16 | ## The Solution 17 | 18 | ccusage analyzes the local JSONL files that Claude Code automatically generates and provides: 19 | 20 | - **Detailed Usage Reports** - Daily, monthly, and session-based breakdowns 21 | - **Cost Analysis** - Estimated costs based on token usage and model pricing 22 | - **Live Monitoring** - Real-time tracking of active sessions 23 | - **Multiple Formats** - Beautiful tables or JSON for further analysis 24 | 25 | ## How It Works 26 | 27 | ```mermaid 28 | graph LR 29 | A[Claude Code] --> B[Local JSONL Files] 30 | B --> C[ccusage] 31 | C --> D[Usage Reports] 32 | C --> E[Cost Analysis] 33 | C --> F[Live Monitoring] 34 | ``` 35 | 36 | 1. **Claude Code generates JSONL files** containing usage data 37 | 2. **ccusage reads these files** from your local machine 38 | 3. **Analyzes and aggregates** the data by date, session, or time blocks 39 | 4. **Calculates estimated costs** using model pricing information 40 | 5. **Presents results** in beautiful tables or JSON format 41 | 42 | ## Key Features 43 | 44 | ### 🚀 Ultra-Small Bundle Size 45 | 46 | Unlike other CLI tools, we pay extreme attention to bundle size. ccusage achieves an incredibly small footprint even without minification, which means you can run it directly without installation using `bunx ccusage` for instant access. 47 | 48 | ### 📊 Multiple Report Types 49 | 50 | - **Daily Reports** - Usage aggregated by calendar date 51 | - **Weekly Reports** - Usage aggregated by week with configurable start day 52 | - **Monthly Reports** - Monthly summaries with trends 53 | - **Session Reports** - Per-conversation analysis 54 | - **Blocks Reports** - 5-hour billing window tracking 55 | 56 | ### 💰 Cost Analysis 57 | 58 | - Estimated costs based on token counts and model pricing 59 | - Support for different cost calculation modes 60 | - Model-specific pricing (Opus vs Sonnet vs other models) 61 | - Cache token cost calculation 62 | 63 | ### 📈 Live Monitoring 64 | 65 | - Real-time dashboard for active sessions 66 | - Progress bars and burn rate calculations 67 | - Token limit warnings and projections 68 | - Automatic refresh with configurable intervals 69 | 70 | ### 🔧 Flexible Configuration 71 | 72 | - **JSON Configuration Files** - Set defaults for all commands or customize per-command 73 | - **IDE Support** - JSON Schema for autocomplete and validation 74 | - **Priority-based Settings** - CLI args > local config > user config > defaults 75 | - **Multiple Claude Data Directories** - Automatic detection and aggregation 76 | - **Environment Variables** - Traditional configuration options 77 | - **Custom Date Filtering** - Flexible time range selection and sorting 78 | - **Offline Mode** - Cached pricing data for air-gapped environments 79 | 80 | ## Data Sources 81 | 82 | ccusage reads from Claude Code's local data directories: 83 | 84 | - **New location**: `~/.config/claude/projects/` (Claude Code v1.0.30+) 85 | - **Legacy location**: `~/.claude/projects/` (pre-v1.0.30) 86 | 87 | The tool automatically detects and aggregates data from both locations for compatibility. 88 | 89 | ## Privacy & Security 90 | 91 | - **100% Local** - All analysis happens on your machine 92 | - **No Data Transmission** - Your usage data never leaves your computer 93 | - **Read-Only** - ccusage only reads files, never modifies them 94 | - **Open Source** - Full transparency in how your data is processed 95 | 96 | ## Limitations 97 | 98 | ::: warning Important Limitations 99 | 100 | - **Local Files Only** - Only analyzes data from your current machine 101 | - **Language Model Tokens** - API calls for tools like Web Search are not included 102 | - **Estimate Accuracy** - Costs are estimates and may not reflect actual billing 103 | ::: 104 | 105 | ## Acknowledgments 106 | 107 | Thanks to [@milliondev](https://note.com/milliondev) for the [original concept and approach](https://note.com/milliondev/n/n1d018da2d769) to Claude Code usage analysis. 108 | 109 | ## Getting Started 110 | 111 | Ready to analyze your Claude Code usage? Check out our [Getting Started Guide](/guide/getting-started) to begin exploring your data! 112 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ccusage can be installed and used in several ways depending on your preferences and use case. 4 | 5 | ## Why No Installation Needed? 6 | 7 | Thanks to ccusage's incredibly small bundle size, you don't need to install it globally. Unlike other CLI tools, we pay extreme attention to bundle size optimization, achieving an impressively small footprint even without minification. This means: 8 | 9 | - ✅ Near-instant startup times 10 | - ✅ Minimal download overhead 11 | - ✅ Always use the latest version 12 | - ✅ No global pollution of your system 13 | 14 | ## Quick Start (Recommended) 15 | 16 | The fastest way to use ccusage is to run it directly: 17 | 18 | ::: code-group 19 | 20 | ```bash [bunx (Recommended)] 21 | bunx ccusage 22 | ``` 23 | 24 | ```bash [npx] 25 | npx ccusage@latest 26 | ``` 27 | 28 | ```bash [pnpm] 29 | pnpm dlx ccusage 30 | ``` 31 | 32 | ```bash [deno] 33 | deno run -E -R=$HOME/.claude/projects/ -S=homedir -N='raw.githubusercontent.com:443' npm:ccusage@latest 34 | ``` 35 | 36 | ::: 37 | 38 | ::: tip Speed Recommendation 39 | We strongly recommend using `bunx` instead of `npx` due to the massive speed difference. Bunx caches packages more efficiently, resulting in near-instant startup times after the first run. 40 | ::: 41 | 42 | ::: info Deno Security 43 | Consider using `deno run` if you want additional security controls. Deno allows you to specify exact permissions, making it safer to run tools you haven't audited. 44 | ::: 45 | 46 | ### Performance Comparison 47 | 48 | Here's why runtime choice matters: 49 | 50 | | Runtime | First Run | Subsequent Runs | Notes | 51 | |---------|-----------|-----------------|-------| 52 | | bunx | Fast | **Instant** | Best overall choice | 53 | | npx | Slow | Moderate | Widely available | 54 | | pnpm dlx | Fast | Fast | Good alternative | 55 | | deno | Moderate | Fast | Best for security | 56 | 57 | ## Global Installation (Optional) 58 | 59 | While not necessary due to our small bundle size, you can still install ccusage globally if you prefer: 60 | 61 | ::: code-group 62 | 63 | ```bash [npm] 64 | npm install -g ccusage 65 | ``` 66 | 67 | ```bash [bun] 68 | bun install -g ccusage 69 | ``` 70 | 71 | ```bash [yarn] 72 | yarn global add ccusage 73 | ``` 74 | 75 | ```bash [pnpm] 76 | pnpm add -g ccusage 77 | ``` 78 | 79 | ::: 80 | 81 | After global installation, run commands directly: 82 | 83 | ```bash 84 | ccusage daily 85 | ccusage monthly --breakdown 86 | ccusage blocks --live 87 | ``` 88 | 89 | ## Development Installation 90 | 91 | For development or contributing to ccusage: 92 | 93 | ```bash 94 | # Clone the repository 95 | git clone https://github.com/ryoppippi/ccusage.git 96 | cd ccusage 97 | 98 | # Install dependencies 99 | bun install 100 | 101 | # Run directly from source 102 | bun run start daily 103 | bun run start monthly --json 104 | ``` 105 | 106 | ### Development Scripts 107 | 108 | ```bash 109 | # Run tests 110 | bun run test 111 | 112 | # Type checking 113 | bun typecheck 114 | 115 | # Build distribution 116 | bun run build 117 | 118 | # Lint and format 119 | bun run format 120 | ``` 121 | 122 | ## Runtime Requirements 123 | 124 | ### Node.js 125 | 126 | - **Minimum**: Node.js 20.x 127 | - **Recommended**: Node.js 20.x or later 128 | - **LTS versions** are fully supported 129 | 130 | ### Bun (Alternative) 131 | 132 | - **Minimum**: Bun 1.2+ 133 | - **Recommended**: Latest stable release 134 | - Often faster than Node.js for ccusage 135 | 136 | ### Deno 137 | 138 | Deno 2.0+ is fully supported with proper permissions: 139 | 140 | ```bash 141 | deno run \ 142 | -E \ 143 | -R=$HOME/.claude/projects/ \ 144 | -S=homedir \ 145 | -N='raw.githubusercontent.com:443' \ 146 | npm:ccusage@latest 147 | ``` 148 | 149 | Also you can use `offline` mode to run ccusage without network access: 150 | 151 | ```bash 152 | deno run \ 153 | -E \ 154 | -R=$HOME/.claude/projects/ \ 155 | -S=homedir \ 156 | npm:ccusage@latest --offline 157 | ``` 158 | 159 | ## Verification 160 | 161 | After installation, verify ccusage is working: 162 | 163 | ```bash 164 | # Check version 165 | ccusage --version 166 | 167 | # Run help command 168 | ccusage --help 169 | 170 | # Test with daily report 171 | ccusage daily 172 | ``` 173 | 174 | ## Updating 175 | 176 | ### Direct Execution (npx/bunx) 177 | 178 | Always gets the latest version automatically. 179 | 180 | ### Global Installation 181 | 182 | ```bash 183 | # Update with npm 184 | npm update -g ccusage 185 | 186 | # Update with bun 187 | bun update -g ccusage 188 | ``` 189 | 190 | ### Check Current Version 191 | 192 | ```bash 193 | ccusage --version 194 | ``` 195 | 196 | ## Uninstalling 197 | 198 | ### Global Installation 199 | 200 | ::: code-group 201 | 202 | ```bash [npm] 203 | npm uninstall -g ccusage 204 | ``` 205 | 206 | ```bash [bun] 207 | bun remove -g ccusage 208 | ``` 209 | 210 | ```bash [yarn] 211 | yarn global remove ccusage 212 | ``` 213 | 214 | ```bash [pnpm] 215 | pnpm remove -g ccusage 216 | ``` 217 | 218 | ::: 219 | 220 | ### Development Installation 221 | 222 | ```bash 223 | # Remove cloned repository 224 | rm -rf ccusage/ 225 | ``` 226 | 227 | ## Troubleshooting Installation 228 | 229 | ### Permission Errors 230 | 231 | If you get permission errors during global installation: 232 | 233 | ::: code-group 234 | 235 | ```bash [npm] 236 | # Use npx instead of global install 237 | npx ccusage@latest 238 | 239 | # Or configure npm to use a different directory 240 | npm config set prefix ~/.npm-global 241 | export PATH=~/.npm-global/bin:$PATH 242 | ``` 243 | 244 | ```bash [Node Version Managers] 245 | # Use nvm (recommended) 246 | nvm install node 247 | npm install -g ccusage 248 | 249 | # Or use fnm 250 | fnm install node 251 | npm install -g ccusage 252 | ``` 253 | 254 | ::: 255 | 256 | ### Network Issues 257 | 258 | If installation fails due to network issues: 259 | 260 | ```bash 261 | # Try with different registry 262 | npm install -g ccusage --registry https://registry.npmjs.org 263 | 264 | # Or use bunx for offline-capable runs 265 | bunx ccusage 266 | ``` 267 | 268 | ### Version Conflicts 269 | 270 | If you have multiple versions installed: 271 | 272 | ```bash 273 | # Check which version is being used 274 | which ccusage 275 | ccusage --version 276 | 277 | # Uninstall and reinstall 278 | npm uninstall -g ccusage 279 | npm install -g ccusage@latest 280 | ``` 281 | 282 | ## Next Steps 283 | 284 | After installation, check out: 285 | 286 | - [Getting Started Guide](/guide/getting-started) - Your first usage report 287 | - [Configuration](/guide/configuration) - Customize ccusage behavior 288 | - [Daily Reports](/guide/daily-reports) - Understand daily usage patterns 289 | -------------------------------------------------------------------------------- /docs/guide/library-usage.md: -------------------------------------------------------------------------------- 1 | # Library Usage 2 | 3 | While **ccusage** is primarily known as a CLI tool, it can also be used as a library in your JavaScript/TypeScript projects. This allows you to integrate Claude Code usage analysis directly into your applications. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install ccusage 9 | # or 10 | yarn add ccusage 11 | # or 12 | pnpm add ccusage 13 | # or 14 | bun add ccusage 15 | ``` 16 | 17 | ## Basic Usage 18 | 19 | The library provides functions to load and analyze Claude Code usage data: 20 | 21 | ```typescript 22 | import { loadDailyUsageData, loadMonthlyUsageData, loadSessionData } from 'ccusage/data-loader'; 23 | 24 | // Load daily usage data 25 | const dailyData = await loadDailyUsageData(); 26 | console.log(dailyData); 27 | 28 | // Load monthly usage data 29 | const monthlyData = await loadMonthlyUsageData(); 30 | console.log(monthlyData); 31 | 32 | // Load session data 33 | const sessionData = await loadSessionData(); 34 | console.log(sessionData); 35 | ``` 36 | 37 | ## Cost Calculation 38 | 39 | Use the cost calculation utilities to work with token costs: 40 | 41 | ```typescript 42 | import { calculateTotals, getTotalTokens } from 'ccusage/calculate-cost'; 43 | 44 | // Assume 'usageEntries' is an array of usage data objects 45 | const totals = calculateTotals(usageEntries); 46 | 47 | // Get total tokens from the same entries 48 | const totalTokens = getTotalTokens(usageEntries); 49 | ``` 50 | 51 | ## Advanced Configuration 52 | 53 | You can customize the data loading behavior: 54 | 55 | ```typescript 56 | import { loadDailyUsageData } from 'ccusage/data-loader'; 57 | 58 | // Load data with custom options 59 | const data = await loadDailyUsageData({ 60 | mode: 'calculate', // Force cost calculation 61 | claudePaths: ['/custom/path/to/claude'], // Custom Claude data paths 62 | }); 63 | ``` 64 | 65 | ## TypeScript Support 66 | 67 | The library is fully typed with TypeScript definitions: 68 | 69 | ```typescript 70 | import type { DailyUsage, ModelBreakdown, MonthlyUsage, SessionUsage, UsageData } from 'ccusage/data-loader'; 71 | 72 | // Use the types in your application 73 | function processUsageData(data: UsageData[]): void { 74 | // Your processing logic here 75 | } 76 | ``` 77 | 78 | ## MCP Server Integration 79 | 80 | You can also create your own MCP server using the library: 81 | 82 | ```typescript 83 | import { createMcpServer } from 'ccusage/mcp'; 84 | 85 | // Create an MCP server instance 86 | const server = createMcpServer(); 87 | 88 | // Start the server 89 | server.start(); 90 | ``` 91 | 92 | ## API Reference 93 | 94 | For detailed information about all available functions, types, and options, see the [API Reference](/api/) section. 95 | 96 | ## Examples 97 | 98 | Here are some common use cases: 99 | 100 | ### Building a Web Dashboard 101 | 102 | ```typescript 103 | import { loadDailyUsageData } from 'ccusage/data-loader'; 104 | 105 | export async function GET() { 106 | const data = await loadDailyUsageData(); 107 | return Response.json(data); 108 | } 109 | ``` 110 | 111 | ### Creating Custom Reports 112 | 113 | ```typescript 114 | import { calculateTotals, loadSessionData } from 'ccusage'; 115 | 116 | async function generateCustomReport() { 117 | const sessions = await loadSessionData(); 118 | 119 | const report = sessions.map(session => ({ 120 | project: session.project, 121 | session: session.session, 122 | totalCost: calculateTotals(session.usage).costUSD, 123 | })); 124 | 125 | return report; 126 | } 127 | ``` 128 | 129 | ### Monitoring Usage Programmatically 130 | 131 | ```typescript 132 | import { loadDailyUsageData } from 'ccusage/data-loader'; 133 | 134 | async function checkUsageAlert() { 135 | const dailyData = await loadDailyUsageData(); 136 | const today = dailyData[0]; // Most recent day 137 | 138 | if (today.totalCostUSD > 10) { 139 | console.warn(`High usage detected: $${today.totalCostUSD}`); 140 | } 141 | } 142 | ``` 143 | 144 | ## Next Steps 145 | 146 | - Explore the [API Reference](/api/) for complete documentation 147 | - Check out the [MCP Server guide](/guide/mcp-server) for integration examples 148 | - See [JSON Output](/guide/json-output) for data format details 149 | -------------------------------------------------------------------------------- /docs/guide/live-monitoring.md: -------------------------------------------------------------------------------- 1 | # Live Monitoring 2 | 3 | ![Live monitoring dashboard showing real-time token usage, burn rate, and cost projections](/blocks-live.png) 4 | 5 | Live monitoring provides a real-time dashboard that updates as you use Claude Code, showing progress bars, burn rates, and cost projections for your active session. 6 | 7 | ## Quick Start 8 | 9 | ```bash 10 | ccusage blocks --live 11 | ``` 12 | 13 | This starts live monitoring with automatic token limit detection based on your usage history. 14 | 15 | ## Features 16 | 17 | ### Real-time Updates 18 | 19 | The dashboard refreshes every second, showing: 20 | 21 | - **Current session progress** with visual progress bar 22 | - **Token burn rate** (tokens per minute) 23 | - **Time remaining** in current 5-hour block 24 | - **Cost projections** based on current usage patterns 25 | - **Quota warnings** with color-coded alerts 26 | 27 | ### Visual Example 28 | 29 | ![Live monitoring dashboard showing real-time token usage, burn rate, and cost projections](/blocks-live.png) 30 | 31 | ## Command Options 32 | 33 | ### Token Limits 34 | 35 | Set custom token limits for quota warnings: 36 | 37 | ```bash 38 | # Use specific token limit 39 | ccusage blocks --live -t 500000 40 | 41 | # Use highest previous session as limit (default) 42 | ccusage blocks --live -t max 43 | 44 | # Explicitly set max (same as default) 45 | ccusage blocks --live -t max 46 | ``` 47 | 48 | ### Refresh Interval 49 | 50 | Control update frequency: 51 | 52 | ```bash 53 | # Update every 5 seconds 54 | ccusage blocks --live --refresh-interval 5 55 | 56 | # Update every 10 seconds (lighter on CPU) 57 | ccusage blocks --live --refresh-interval 10 58 | 59 | # Fast updates (every 0.5 seconds) 60 | ccusage blocks --live --refresh-interval 0.5 61 | ``` 62 | 63 | ::: tip Refresh Rate 64 | 65 | - **1 second (default)**: Good balance of responsiveness and performance 66 | - **0.5-2 seconds**: For active monitoring during heavy usage 67 | - **5-10 seconds**: For casual monitoring or slower systems 68 | ::: 69 | 70 | ### Combined Options 71 | 72 | ```bash 73 | # Custom limit with slower refresh 74 | ccusage blocks --live -t 750000 --refresh-interval 3 75 | 76 | # Maximum responsiveness 77 | ccusage blocks --live -t max --refresh-interval 0.5 78 | ``` 79 | 80 | ## Understanding the Display 81 | 82 | ### Progress Bar 83 | 84 | The progress bar shows token usage within the current 5-hour block: 85 | 86 | - **Green**: Normal usage (0-60% of limit) 87 | - **Yellow**: Moderate usage (60-80% of limit) 88 | - **Red**: High usage (80-100% of limit) 89 | 90 | ### Metrics Explained 91 | 92 | #### Current Session 93 | 94 | - **Tokens used** in the current 5-hour block 95 | - **Percentage** of token limit consumed 96 | 97 | #### Time Remaining 98 | 99 | - **Hours and minutes** left in current block 100 | - Resets every 5 hours from first message 101 | 102 | #### Burn Rate 103 | 104 | - **Tokens per minute** based on recent activity 105 | - Calculated from last 10 minutes of usage 106 | - Used for projections 107 | 108 | #### Cost Tracking 109 | 110 | - **Current Cost**: Actual cost so far in this block 111 | - **Projected Cost**: Estimated total cost if current rate continues 112 | 113 | ### Warning System 114 | 115 | ccusage shows color-coded warnings based on usage: 116 | 117 | - 🟢 **< 60%**: Normal usage 118 | - 🟡 **60-80%**: Moderate usage warning 119 | - 🔴 **80-100%**: High usage warning 120 | - ⚠️ **> 100%**: Over limit warning 121 | 122 | ## Use Cases 123 | 124 | ### Active Development 125 | 126 | Monitor usage during intensive coding sessions: 127 | 128 | ```bash 129 | # Monitor with reasonable limit 130 | ccusage blocks --live -t 500000 131 | ``` 132 | 133 | Perfect for: 134 | 135 | - Large refactoring projects 136 | - Documentation generation 137 | - Code review sessions 138 | 139 | ### Team Collaboration 140 | 141 | Track usage during pair programming: 142 | 143 | ```bash 144 | # Higher limit for team sessions 145 | ccusage blocks --live -t 1000000 146 | ``` 147 | 148 | ### Budget Management 149 | 150 | Set strict limits for cost control: 151 | 152 | ```bash 153 | # Conservative monitoring 154 | ccusage blocks --live -t 200000 155 | ``` 156 | 157 | ### Learning Sessions 158 | 159 | Monitor while learning new technologies: 160 | 161 | ```bash 162 | # Moderate limit with frequent updates 163 | ccusage blocks --live -t 300000 --refresh-interval 2 164 | ``` 165 | 166 | ## Tips for Effective Monitoring 167 | 168 | ### 1. Set Appropriate Limits 169 | 170 | Choose token limits based on your needs: 171 | 172 | - **Conservative (100k-300k)**: Light usage, cost-conscious 173 | - **Moderate (300k-700k)**: Regular development work 174 | - **High (700k-1M+)**: Intensive projects, team sessions 175 | 176 | ### 2. Monitor Burn Rate 177 | 178 | Watch for sudden increases in burn rate: 179 | 180 | - **Steady rate**: Normal conversation flow 181 | - **Spikes**: Complex queries or large code generation 182 | - **High sustained rate**: Consider taking breaks 183 | 184 | ### 3. Use Projections Wisely 185 | 186 | Projections help estimate session costs: 187 | 188 | - **Early session**: Projections may be inaccurate 189 | - **Mid-session**: More reliable estimates 190 | - **Late session**: Highly accurate projections 191 | 192 | ### 4. Plan Around Blocks 193 | 194 | Remember that 5-hour blocks reset: 195 | 196 | - **Block boundary**: Good time for breaks 197 | - **New block**: Fresh token allowance 198 | - **Block overlap**: Previous usage doesn't carry over 199 | 200 | ## Keyboard Controls 201 | 202 | While live monitoring is active: 203 | 204 | - **Ctrl+C**: Exit monitoring gracefully 205 | - **Terminal resize**: Automatically adjusts display 206 | 207 | ## Performance Notes 208 | 209 | ### CPU Usage 210 | 211 | Live monitoring uses minimal resources: 212 | 213 | - **1-second refresh**: ~0.1% CPU usage 214 | - **0.5-second refresh**: ~0.2% CPU usage 215 | - **File watching**: Efficient incremental updates 216 | 217 | ### Network Usage 218 | 219 | - **Offline mode**: Zero network usage 220 | - **Online mode**: Minimal API calls for pricing 221 | - **Local analysis**: All processing happens locally 222 | 223 | ## Troubleshooting 224 | 225 | ### No Active Session 226 | 227 | If no active session is detected: 228 | 229 | ``` 230 | No active session found. Start using Claude Code to begin monitoring. 231 | ``` 232 | 233 | **Solutions**: 234 | 235 | 1. Send a message in Claude Code 236 | 2. Wait a few seconds for data to be written 237 | 3. Check that Claude Code is running 238 | 239 | ### Incorrect Token Limits 240 | 241 | If automatic limit detection fails: 242 | 243 | ```bash 244 | # Manually set a reasonable limit 245 | ccusage blocks --live -t 500000 246 | ``` 247 | 248 | ### Performance Issues 249 | 250 | If monitoring feels slow: 251 | 252 | ```bash 253 | # Reduce refresh frequency 254 | ccusage blocks --live --refresh-interval 5 255 | ``` 256 | 257 | ## Related Commands 258 | 259 | - [Blocks Reports](/guide/blocks-reports) - Static 5-hour block analysis 260 | - [Session Reports](/guide/session-reports) - Historical session data 261 | - [Daily Reports](/guide/daily-reports) - Day-by-day usage patterns 262 | 263 | ## Advanced Usage 264 | 265 | Combine live monitoring with other tools: 266 | 267 | ```bash 268 | # Monitor in background, export data periodically 269 | ccusage blocks --live & 270 | ccusage session --json > session-backup.json 271 | ``` 272 | -------------------------------------------------------------------------------- /docs/guide/mcp-server.md: -------------------------------------------------------------------------------- 1 | # MCP Server 2 | 3 | ccusage includes a built-in Model Context Protocol (MCP) server that exposes usage data through standardized tools. This allows integration with other applications that support MCP. 4 | 5 | ## Starting the MCP Server 6 | 7 | ### stdio transport (default) 8 | 9 | ```bash 10 | ccusage mcp 11 | # or explicitly (--type stdio is optional): 12 | ccusage mcp --type stdio 13 | ``` 14 | 15 | The stdio transport is ideal for local integration where the client directly spawns the process. 16 | 17 | ### HTTP Stream Transport 18 | 19 | ```bash 20 | ccusage mcp --type http --port 8080 21 | ``` 22 | 23 | The HTTP stream transport is best for remote access when you need to call the server from another machine or network location. 24 | 25 | ### Cost Calculation Mode 26 | 27 | You can control how costs are calculated: 28 | 29 | ```bash 30 | # Use pre-calculated costs when available, calculate from tokens otherwise (default) 31 | ccusage mcp --mode auto 32 | 33 | # Always calculate costs from tokens using model pricing 34 | ccusage mcp --mode calculate 35 | 36 | # Always use pre-calculated costUSD values only 37 | ccusage mcp --mode display 38 | ``` 39 | 40 | ## Available MCP Tools 41 | 42 | The MCP server provides four main tools for analyzing Claude Code usage: 43 | 44 | ### daily 45 | 46 | Returns daily usage reports with aggregated token usage and costs by date. 47 | 48 | **Parameters:** 49 | 50 | - `since` (optional): Filter from date (YYYYMMDD format) 51 | - `until` (optional): Filter until date (YYYYMMDD format) 52 | - `mode` (optional): Cost calculation mode (`auto`, `calculate`, or `display`) 53 | 54 | ### monthly 55 | 56 | Returns monthly usage reports with aggregated token usage and costs by month. 57 | 58 | **Parameters:** 59 | 60 | - `since` (optional): Filter from date (YYYYMMDD format) 61 | - `until` (optional): Filter until date (YYYYMMDD format) 62 | - `mode` (optional): Cost calculation mode (`auto`, `calculate`, or `display`) 63 | 64 | ### session 65 | 66 | Returns session-based usage reports grouped by conversation sessions. 67 | 68 | **Parameters:** 69 | 70 | - `since` (optional): Filter from date (YYYYMMDD format) 71 | - `until` (optional): Filter until date (YYYYMMDD format) 72 | - `mode` (optional): Cost calculation mode (`auto`, `calculate`, or `display`) 73 | 74 | ### blocks 75 | 76 | Returns 5-hour billing blocks usage reports showing usage within Claude's billing windows. 77 | 78 | **Parameters:** 79 | 80 | - `since` (optional): Filter from date (YYYYMMDD format) 81 | - `until` (optional): Filter until date (YYYYMMDD format) 82 | - `mode` (optional): Cost calculation mode (`auto`, `calculate`, or `display`) 83 | 84 | ## Testing the MCP Server 85 | 86 | ### Interactive Testing with MCP Inspector 87 | 88 | You can test the MCP server using the MCP Inspector for interactive debugging: 89 | 90 | ```bash 91 | # Test with web UI (if you have the dev environment set up) 92 | bun run mcp 93 | 94 | # Test with the official MCP Inspector 95 | bunx @modelcontextprotocol/inspector bunx ccusage mcp 96 | ``` 97 | 98 | The MCP Inspector provides a web-based interface to: 99 | 100 | - Test individual MCP tools (daily, monthly, session, blocks) 101 | - Inspect tool schemas and parameters 102 | - Debug server responses 103 | - Export server configurations 104 | 105 | ### Manual Testing 106 | 107 | You can also manually test the server by running it and sending JSON-RPC messages: 108 | 109 | ```bash 110 | # Start the server 111 | ccusage mcp 112 | 113 | # The server will wait for JSON-RPC messages on stdin 114 | # Example: List available tools 115 | {"jsonrpc": "2.0", "id": 1, "method": "tools/list"} 116 | ``` 117 | 118 | ## Integration Examples 119 | 120 | ### With Claude Desktop 121 | 122 | ![Claude Desktop MCP Configuration](/mcp-claude-desktop.avif) 123 | 124 | To use ccusage MCP with Claude Desktop, add this to your Claude Desktop configuration file: 125 | 126 | **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 127 | **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 128 | 129 | #### Using npx (Recommended) 130 | 131 | ```json 132 | { 133 | "mcpServers": { 134 | "ccusage": { 135 | "command": "npx", 136 | "args": ["ccusage@latest", "mcp"], 137 | "env": {} 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | #### Using Global Installation 144 | 145 | If you have ccusage installed globally: 146 | 147 | ```json 148 | { 149 | "mcpServers": { 150 | "ccusage": { 151 | "command": "ccusage", 152 | "args": ["mcp"], 153 | "env": {} 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | #### Custom Configuration 160 | 161 | You can specify custom Claude data directories and cost calculation modes: 162 | 163 | ```json 164 | { 165 | "mcpServers": { 166 | "ccusage": { 167 | "command": "npx", 168 | "args": ["ccusage@latest", "mcp", "--mode", "calculate"], 169 | "env": { 170 | "CLAUDE_CONFIG_DIR": "/path/to/your/claude/data" 171 | } 172 | } 173 | } 174 | } 175 | ``` 176 | 177 | After adding this configuration, restart Claude Desktop. You'll then be able to use the ccusage tools within Claude to analyze your usage data. 178 | 179 | #### Available Commands in Claude Desktop 180 | 181 | Once configured, you can ask Claude to: 182 | 183 | - "Show me my Claude Code usage for today" 184 | - "Generate a monthly usage report" 185 | - "Which sessions used the most tokens?" 186 | - "Show me my current billing block usage" 187 | - "Analyze my 5-hour block patterns" 188 | 189 | #### Troubleshooting Claude Desktop Integration 190 | 191 | **Configuration Not Working:** 192 | 193 | 1. Verify the config file is in the correct location for your OS 194 | 2. Check JSON syntax with a validator 195 | 3. Restart Claude Desktop completely 196 | 4. Ensure ccusage is installed and accessible 197 | 198 | **Common Issues:** 199 | 200 | - "Command not found": Install ccusage globally or use the npx configuration 201 | - "No usage data found": Verify your Claude Code data directory exists 202 | - Performance issues: Consider using `--mode display` or `--offline` flag 203 | 204 | ### With Other MCP Clients 205 | 206 | Any application that supports the Model Context Protocol can integrate with ccusage's MCP server. The server follows the MCP specification for tool discovery and execution. 207 | 208 | ## Environment Variables 209 | 210 | The MCP server respects the same environment variables as the CLI: 211 | 212 | - `CLAUDE_CONFIG_DIR`: Specify custom Claude data directory paths 213 | ```bash 214 | export CLAUDE_CONFIG_DIR="/path/to/claude" 215 | ccusage mcp 216 | ``` 217 | 218 | ## Error Handling 219 | 220 | The MCP server handles errors gracefully: 221 | 222 | - Invalid date formats in parameters return descriptive error messages 223 | - Missing Claude data directories are handled with appropriate warnings 224 | - Malformed JSONL files are skipped during data loading 225 | - Network errors (when fetching pricing data) fall back to cached data when using `auto` mode 226 | -------------------------------------------------------------------------------- /docs/guide/monthly-reports.md: -------------------------------------------------------------------------------- 1 | # Monthly Reports 2 | 3 | Monthly reports aggregate your Claude Code usage by calendar month, providing a high-level view of your usage patterns and costs over longer time periods. 4 | 5 | ## Basic Usage 6 | 7 | ```bash 8 | ccusage monthly 9 | ``` 10 | 11 | ## Example Output 12 | 13 | ``` 14 | ╭─────────────────────────────────────────────╮ 15 | │ │ 16 | │ Claude Code Token Usage Report - Monthly │ 17 | │ │ 18 | ╰─────────────────────────────────────────────╯ 19 | 20 | ┌─────────┬──────────────────┬─────────┬──────────┬──────────────┬────────────┬──────────────┬────────────┐ 21 | │ Month │ Models │ Input │ Output │ Cache Create │ Cache Read │ Total Tokens │ Cost (USD) │ 22 | ├─────────┼──────────────────┼─────────┼──────────┼──────────────┼────────────┼──────────────┼────────────┤ 23 | │ 2025-06 │ • opus-4 │ 45,231 │ 892,456 │ 2,048 │ 4,096 │ 943,831 │ $1,247.92│ 24 | │ │ • sonnet-4 │ │ │ │ │ │ │ 25 | │ 2025-05 │ • sonnet-4 │ 38,917 │ 756,234 │ 1,536 │ 3,072 │ 799,759 │ $892.15│ 26 | │ 2025-04 │ • opus-4 │ 22,458 │ 534,789 │ 1,024 │ 2,048 │ 560,319 │ $678.43│ 27 | ├─────────┼──────────────────┼─────────┼──────────┼──────────────┼────────────┼──────────────┼────────────┤ 28 | │ Total │ │ 106,606 │2,183,479 │ 4,608 │ 9,216 │ 2,303,909 │ $2,818.50│ 29 | └─────────┴──────────────────┴─────────┴──────────┴──────────────┴────────────┴──────────────┴────────────┘ 30 | ``` 31 | 32 | ## Understanding Monthly Data 33 | 34 | ### Month Format 35 | 36 | Months are displayed in YYYY-MM format: 37 | 38 | - `2025-06` = June 2025 39 | - `2025-05` = May 2025 40 | 41 | ### Aggregation Logic 42 | 43 | All usage within a calendar month is aggregated: 44 | 45 | - Input/output tokens summed across all days 46 | - Costs calculated from total token usage 47 | - Models listed if used at any point in the month 48 | 49 | ## Command Options 50 | 51 | ### Date Filtering 52 | 53 | Filter by month range: 54 | 55 | ```bash 56 | # Show specific months 57 | ccusage monthly --since 20250101 --until 20250630 58 | 59 | # Show usage from 2024 60 | ccusage monthly --since 20240101 --until 20241231 61 | 62 | # Show last 6 months 63 | ccusage monthly --since $(date -d '6 months ago' +%Y%m%d) 64 | ``` 65 | 66 | ::: tip Date Filtering 67 | Even though you specify full dates (YYYYMMDD), monthly reports group by month. The filters determine which months to include. 68 | ::: 69 | 70 | ### Sort Order 71 | 72 | ```bash 73 | # Newest months first (default) 74 | ccusage monthly --order desc 75 | 76 | # Oldest months first 77 | ccusage monthly --order asc 78 | ``` 79 | 80 | ### Cost Calculation Modes 81 | 82 | ```bash 83 | # Use pre-calculated costs when available (default) 84 | ccusage monthly --mode auto 85 | 86 | # Always calculate costs from tokens 87 | ccusage monthly --mode calculate 88 | 89 | # Only show pre-calculated costs 90 | ccusage monthly --mode display 91 | ``` 92 | 93 | ### Model Breakdown 94 | 95 | See costs broken down by model: 96 | 97 | ```bash 98 | ccusage monthly --breakdown 99 | ``` 100 | 101 | Example with breakdown: 102 | 103 | ``` 104 | ┌─────────┬──────────────────┬─────────┬──────────┬────────────┐ 105 | │ Month │ Models │ Input │ Output │ Cost (USD) │ 106 | ├─────────┼──────────────────┼─────────┼──────────┼────────────┤ 107 | │ 2025-06 │ opus-4, sonnet-4 │ 45,231 │ 892,456 │ $1,247.92 │ 108 | ├─────────┼──────────────────┼─────────┼──────────┼────────────┤ 109 | │ └─ opus-4 │ 20,000 │ 400,000 │ $750.50 │ 110 | ├─────────┼──────────────────┼─────────┼──────────┼────────────┤ 111 | │ └─ sonnet-4 │ 25,231 │ 492,456 │ $497.42 │ 112 | └─────────┴──────────────────┴─────────┴──────────┴────────────┘ 113 | ``` 114 | 115 | ### JSON Output 116 | 117 | ```bash 118 | ccusage monthly --json 119 | ``` 120 | 121 | ```json 122 | [ 123 | { 124 | "month": "2025-06", 125 | "models": ["opus-4", "sonnet-4"], 126 | "inputTokens": 45231, 127 | "outputTokens": 892456, 128 | "cacheCreationTokens": 2048, 129 | "cacheReadTokens": 4096, 130 | "totalTokens": 943831, 131 | "totalCost": 1247.92 132 | } 133 | ] 134 | ``` 135 | 136 | ### Offline Mode 137 | 138 | ```bash 139 | ccusage monthly --offline 140 | ``` 141 | 142 | ## Analysis Use Cases 143 | 144 | ### Budget Planning 145 | 146 | Monthly reports help with subscription planning: 147 | 148 | ```bash 149 | # Check last year's usage 150 | ccusage monthly --since 20240101 --until 20241231 151 | ``` 152 | 153 | Look at the total cost to understand what you'd pay on usage-based pricing. 154 | 155 | ### Usage Trends 156 | 157 | Track how your usage changes over time: 158 | 159 | ```bash 160 | # Compare year over year 161 | ccusage monthly --since 20230101 --until 20231231 # 2023 162 | ccusage monthly --since 20240101 --until 20241231 # 2024 163 | ``` 164 | 165 | ### Model Migration Analysis 166 | 167 | See how your model usage evolves: 168 | 169 | ```bash 170 | ccusage monthly --breakdown 171 | ``` 172 | 173 | This helps track transitions between Opus, Sonnet, and other models. 174 | 175 | ### Seasonal Patterns 176 | 177 | Identify busy/slow periods: 178 | 179 | ```bash 180 | # Academic year analysis 181 | ccusage monthly --since 20240901 --until 20250630 182 | ``` 183 | 184 | ### Export for Business Analysis 185 | 186 | ```bash 187 | # Create quarterly reports 188 | ccusage monthly --since 20241001 --until 20241231 --json > q4-2024.json 189 | ``` 190 | 191 | ## Tips for Monthly Analysis 192 | 193 | ### 1. Cost Context 194 | 195 | Monthly totals show: 196 | 197 | - **Subscription Value**: How much you'd pay with usage-based billing 198 | - **Usage Intensity**: Months with heavy Claude usage 199 | - **Model Preferences**: Which models you favor over time 200 | 201 | ### 2. Trend Analysis 202 | 203 | Look for patterns: 204 | 205 | - Increasing usage over time 206 | - Seasonal variations 207 | - Model adoption curves 208 | 209 | ### 3. Business Planning 210 | 211 | Use monthly data for: 212 | 213 | - Team budget planning 214 | - Usage forecasting 215 | - Subscription optimization 216 | 217 | ### 4. Comparative Analysis 218 | 219 | Compare monthly reports with: 220 | 221 | - Team productivity metrics 222 | - Project timelines 223 | - Business outcomes 224 | 225 | ## Related Commands 226 | 227 | - [Daily Reports](/guide/daily-reports) - Day-by-day breakdown 228 | - [Session Reports](/guide/session-reports) - Individual conversations 229 | - [Blocks Reports](/guide/blocks-reports) - 5-hour billing periods 230 | 231 | ## Next Steps 232 | 233 | After analyzing monthly trends, consider: 234 | 235 | 1. [Session Reports](/guide/session-reports) to identify high-cost conversations 236 | 2. [Live Monitoring](/guide/live-monitoring) to track real-time usage 237 | 3. [Library Usage](/guide/library-usage) for programmatic analysis 238 | -------------------------------------------------------------------------------- /docs/guide/related-projects.md: -------------------------------------------------------------------------------- 1 | # Related Projects 2 | 3 | Projects that use ccusage internally or extend its functionality: 4 | 5 | ## Desktop Applications 6 | 7 | - [claude-usage-tracker-for-mac](https://github.com/penicillin0/claude-usage-tracker-for-mac) - macOS menu bar app for tracking Claude usage 8 | - [ClaudeCode_Dashboard](https://github.com/m-sigepon/ClaudeCode_Dashboard) - Web dashboard with charts and visualizations 9 | - [Ccusage App](https://github.com/EthanBarlo/ccusage-app) - Native application to display ccusage data in graphs and visualizations 10 | - [CCOwl](https://github.com/sivchari/ccowl) - A cross-platform status bar application that monitors Claude Code usage in real-time. 11 | 12 | ## Extensions & Integrations 13 | 14 | - [ccusage Raycast Extension](https://www.raycast.com/nyatinte/ccusage) - Raycast integration for quick usage checks 15 | - [ccusage.nvim](https://github.com/S1M0N38/ccusage.nvim) - Track Claude Code usage in Neovim 16 | 17 | ## Web Applications 18 | 19 | - [viberank](https://viberank.app) - A community-driven leaderboard for Claude Code usage. ([GitHub](https://github.com/sculptdotfun/viberank)) 20 | 21 | ## Contributing 22 | 23 | If you've built something that uses ccusage, please feel free to open a pull request to add it to this list! 24 | -------------------------------------------------------------------------------- /docs/guide/sponsors.md: -------------------------------------------------------------------------------- 1 | # Sponsors 2 | 3 | Support ccusage development by becoming a sponsor! Your contribution helps maintain and improve this tool. 4 | 5 | ## Featured Sponsor 6 | 7 | Check out [ccusage: The Claude Code cost scorecard that went viral](https://www.youtube.com/watch?v=Ak6qpQ5qdgk) 8 | 9 |

10 | 11 | ccusage: The Claude Code cost scorecard that went viral 12 | 13 |

14 | 15 |

16 | 17 | 18 | 19 |

20 | 21 | ## How to Sponsor 22 | 23 | Visit [GitHub Sponsors - @ryoppippi](https://github.com/sponsors/ryoppippi) to support the development of ccusage and other open source projects. 24 | 25 | ## Star History 26 | 27 | 28 | 29 | 30 | 31 | Star History Chart 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/guide/weekly-reports.md: -------------------------------------------------------------------------------- 1 | # Weekly Reports 2 | 3 | Weekly reports aggregate your Claude Code usage by week, providing a mid-range view between daily and monthly reports. This helps identify weekly patterns and trends in your usage. 4 | 5 | ## Basic Usage 6 | 7 | Show all weekly usage: 8 | 9 | ```bash 10 | ccusage weekly 11 | ``` 12 | 13 | ## Example Output 14 | 15 | ``` 16 | ┌────────────────┬──────────────────┬────────┬─────────┬─────────────┬────────────┬──────────────┬────────────┐ 17 | │ Week │ Models │ Input │ Output │ Cache Create│ Cache Read │ Total Tokens │ Cost (USD) │ 18 | ├────────────────┼──────────────────┼────────┼─────────┼─────────────┼────────────┼──────────────┼────────────┤ 19 | │ 2025-06-16 │ • opus-4 │ 1,234 │ 156,789 │ 2,048 │ 4,096 │ 164,167 │ $87.56 │ 20 | │ │ • sonnet-4 │ │ │ │ │ │ │ 21 | ├────────────────┼──────────────────┼────────┼─────────┼─────────────┼────────────┼──────────────┼────────────┤ 22 | │ 2025-06-23 │ • sonnet-4 │ 2,456 │ 234,567 │ 3,072 │ 6,144 │ 246,239 │ $104.33 │ 23 | ├────────────────┼──────────────────┼────────┼─────────┼─────────────┼────────────┼──────────────┼────────────┤ 24 | │ 2025-06-30 │ • opus-4 │ 3,789 │ 345,678 │ 4,096 │ 8,192 │ 361,755 │ $156.78 │ 25 | │ │ • sonnet-4 │ │ │ │ │ │ │ 26 | └────────────────┴──────────────────┴────────┴─────────┴─────────────┴────────────┴──────────────┴────────────┘ 27 | ``` 28 | 29 | ## Understanding the Columns 30 | 31 | The columns are identical to daily reports but aggregated by week: 32 | 33 | - **Week**: Start date of the week (configurable) 34 | - **Models**: All Claude models used during the week 35 | - **Input/Output**: Total tokens for the week 36 | - **Cache Create/Read**: Cache token usage 37 | - **Total Tokens**: Sum of all token types 38 | - **Cost (USD)**: Estimated cost for the week 39 | 40 | ## Command Options 41 | 42 | ### Week Start Day 43 | 44 | Configure which day starts the week: 45 | 46 | ```bash 47 | # Start week on Sunday (default) 48 | ccusage weekly --start-of-week sunday 49 | 50 | # Start week on Monday 51 | ccusage weekly --start-of-week monday 52 | ccusage weekly -w monday 53 | 54 | # Other options: tuesday, wednesday, thursday, friday, saturday 55 | ``` 56 | 57 | ### Date Filtering 58 | 59 | Filter by date range: 60 | 61 | ```bash 62 | # Show specific period 63 | ccusage weekly --since 20250601 --until 20250630 64 | 65 | # Show last 4 weeks 66 | ccusage weekly --since 20250501 67 | ``` 68 | 69 | ### Sort Order 70 | 71 | Control the order of weeks: 72 | 73 | ```bash 74 | # Newest weeks first (default) 75 | ccusage weekly --order desc 76 | 77 | # Oldest weeks first 78 | ccusage weekly --order asc 79 | ``` 80 | 81 | ### Model Breakdown 82 | 83 | See per-model weekly costs: 84 | 85 | ```bash 86 | ccusage weekly --breakdown 87 | ``` 88 | 89 | ``` 90 | ┌────────────────┬──────────────────┬────────┬─────────┬────────────┐ 91 | │ Week │ Models │ Input │ Output │ Cost (USD) │ 92 | ├────────────────┼──────────────────┼────────┼─────────┼────────────┤ 93 | │ 2025-06-16 │ opus-4, sonnet-4 │ 1,234 │ 156,789 │ $87.56 │ 94 | ├────────────────┼──────────────────┼────────┼─────────┼────────────┤ 95 | │ └─ opus-4 │ │ 800 │ 80,000 │ $54.80 │ 96 | ├────────────────┼──────────────────┼────────┼─────────┼────────────┤ 97 | │ └─ sonnet-4 │ │ 434 │ 76,789 │ $32.76 │ 98 | └────────────────┴──────────────────┴────────┴─────────┴────────────┘ 99 | ``` 100 | 101 | ### JSON Output 102 | 103 | Export weekly data as JSON: 104 | 105 | ```bash 106 | ccusage weekly --json 107 | ``` 108 | 109 | ```json 110 | { 111 | "weekly": [ 112 | { 113 | "week": "2025-06-16", 114 | "inputTokens": 1234, 115 | "outputTokens": 156789, 116 | "cacheCreationTokens": 2048, 117 | "cacheReadTokens": 4096, 118 | "totalTokens": 164167, 119 | "totalCost": 87.56, 120 | "modelsUsed": ["claude-opus-4-20250514", "claude-sonnet-4-20250514"], 121 | "modelBreakdowns": { 122 | "claude-opus-4-20250514": { 123 | "inputTokens": 800, 124 | "outputTokens": 80000, 125 | "totalCost": 54.80 126 | }, 127 | "claude-sonnet-4-20250514": { 128 | "inputTokens": 434, 129 | "outputTokens": 76789, 130 | "totalCost": 32.76 131 | } 132 | } 133 | } 134 | ], 135 | "totals": { 136 | "inputTokens": 7479, 137 | "outputTokens": 737034, 138 | "cacheCreationTokens": 9216, 139 | "cacheReadTokens": 18432, 140 | "totalTokens": 772161, 141 | "totalCost": 348.67 142 | } 143 | } 144 | ``` 145 | 146 | ### Project Analysis 147 | 148 | Group weekly usage by project: 149 | 150 | ```bash 151 | # Show weekly usage per project 152 | ccusage weekly --instances 153 | 154 | # Filter to specific project 155 | ccusage weekly --project my-project 156 | ``` 157 | 158 | ### Cost Calculation Modes 159 | 160 | Control cost calculation: 161 | 162 | ```bash 163 | # Auto mode (default) 164 | ccusage weekly --mode auto 165 | 166 | # Always calculate from tokens 167 | ccusage weekly --mode calculate 168 | 169 | # Only use pre-calculated costs 170 | ccusage weekly --mode display 171 | ``` 172 | 173 | ### Offline Mode 174 | 175 | Use cached pricing data: 176 | 177 | ```bash 178 | ccusage weekly --offline 179 | ``` 180 | 181 | ## Common Use Cases 182 | 183 | ### Weekly Trends 184 | 185 | ```bash 186 | # See usage trends over past months 187 | ccusage weekly --since 20250401 188 | ``` 189 | 190 | ### Sprint Analysis 191 | 192 | ```bash 193 | # Track usage during 2-week sprints (Monday start) 194 | ccusage weekly --start-of-week monday --since 20250601 195 | ``` 196 | 197 | ### Budget Planning 198 | 199 | ```bash 200 | # Export for weekly budget tracking 201 | ccusage weekly --json > weekly-budget.json 202 | ``` 203 | 204 | ### Compare Workweeks 205 | 206 | ```bash 207 | # Monday-Friday work pattern analysis 208 | ccusage weekly --start-of-week monday --breakdown 209 | ``` 210 | 211 | ### Team Reporting 212 | 213 | ```bash 214 | # Weekly team usage report 215 | ccusage weekly --instances --start-of-week monday 216 | ``` 217 | 218 | ## Tips 219 | 220 | 1. **Week Start**: Choose a start day that aligns with your work schedule 221 | 2. **Breakdown View**: Use `--breakdown` to identify which models drive costs 222 | 3. **JSON Export**: Weekly JSON data is perfect for creating trend charts 223 | 4. **Project Tracking**: Use `--instances` to track project-specific weekly usage 224 | 225 | ## Related Commands 226 | 227 | - [Daily Reports](/guide/daily-reports) - Day-by-day analysis 228 | - [Monthly Reports](/guide/monthly-reports) - Monthly aggregates 229 | - [Session Reports](/guide/session-reports) - Per-conversation analysis 230 | - [Blocks Reports](/guide/blocks-reports) - 5-hour billing windows -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: ccusage 6 | text: Claude Code Usage Analysis 7 | tagline: A powerful CLI tool for analyzing Claude Code usage from local JSONL files 8 | image: 9 | src: /logo.svg 10 | alt: ccusage logo 11 | actions: 12 | - theme: brand 13 | text: Get Started 14 | link: /guide/ 15 | - theme: alt 16 | text: View on GitHub 17 | link: https://github.com/ryoppippi/ccusage 18 | 19 | features: 20 | - icon: 📊 21 | title: Daily Reports 22 | details: View token usage and costs aggregated by date with detailed breakdowns 23 | link: /guide/daily-reports 24 | - icon: 📆 25 | title: Weekly Reports 26 | details: Track usage patterns by week with configurable start day 27 | link: /guide/weekly-reports 28 | - icon: 📅 29 | title: Monthly Reports 30 | details: Analyze usage patterns over monthly periods with cost tracking 31 | link: /guide/monthly-reports 32 | - icon: 💬 33 | title: Session Reports 34 | details: Group usage by conversation sessions for detailed analysis 35 | link: /guide/session-reports 36 | - icon: ⏰ 37 | title: 5-Hour Blocks 38 | details: Track usage within Claude's billing windows with active monitoring 39 | link: /guide/blocks-reports 40 | - icon: 📈 41 | title: Live Monitoring 42 | details: Real-time dashboard with progress bars and cost projections 43 | link: /guide/live-monitoring 44 | - icon: 🤖 45 | title: Model Tracking 46 | details: See which Claude models you're using (Opus, Sonnet, etc.) 47 | - icon: 📋 48 | title: Enhanced Display 49 | details: Beautiful tables with responsive layout and smart formatting 50 | - icon: 📄 51 | title: JSON Output 52 | details: Export data in structured JSON format for programmatic use 53 | link: /guide/json-output 54 | - icon: 💰 55 | title: Cost Analysis 56 | details: Shows estimated costs in USD for each day/month/session 57 | - icon: 🔄 58 | title: Cache Support 59 | details: Tracks cache creation and cache read tokens separately 60 | - icon: 🌐 61 | title: Offline Mode 62 | details: Use pre-cached pricing data without network connectivity 63 | - icon: 🔌 64 | title: MCP Integration 65 | details: Built-in Model Context Protocol server for tool integration 66 | link: /guide/mcp-server 67 | --- 68 | 69 |
70 |

Support ccusage

71 |

If you find ccusage helpful, please consider sponsoring the development!

72 | 73 |

Featured Sponsor

74 |

Check out ccusage: The Claude Code cost scorecard that went viral

75 | 76 | ccusage: The Claude Code cost scorecard that went viral 77 | 78 | 79 |
80 | 81 | Sponsors 82 | 83 |
84 |
85 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ccusage/docs", 3 | "version": "15.0.0", 4 | "private": true, 5 | "description": "Documentation for ccusage", 6 | "type": "module", 7 | "scripts": { 8 | "build": "bun run docs:api && cp ../config-schema.json public/config-schema.json && vitepress build", 9 | "deploy": "wrangler deploy", 10 | "dev": "bun run docs:api && cp ../config-schema.json public/config-schema.json && vitepress dev", 11 | "docs:api": "./update-api-index.ts", 12 | "preview": "vitepress preview" 13 | }, 14 | "devDependencies": { 15 | "@ryoppippi/vite-plugin-cloudflare-redirect": "npm:@jsr/ryoppippi__vite-plugin-cloudflare-redirect", 16 | "@types/bun": "^1.2.20", 17 | "tinyglobby": "^0.2.14", 18 | "typedoc": "^0.28.10", 19 | "typedoc-plugin-markdown": "^4.8.1", 20 | "typedoc-vitepress-theme": "^1.1.2", 21 | "vitepress": "^1.6.4", 22 | "vitepress-plugin-group-icons": "^1.6.3", 23 | "vitepress-plugin-llms": "^1.7.3", 24 | "vitepress-plugin-mermaid": "^2.0.17", 25 | "wrangler": "^4.32.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/public/blocks-live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoppippi/ccusage/075cb93d208b26213743e3072812d8c4a487925c/docs/public/blocks-live.png -------------------------------------------------------------------------------- /docs/public/ccusage_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoppippi/ccusage/075cb93d208b26213743e3072812d8c4a487925c/docs/public/ccusage_thumbnail.png -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoppippi/ccusage/075cb93d208b26213743e3072812d8c4a487925c/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/public/mcp-claude-desktop.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoppippi/ccusage/075cb93d208b26213743e3072812d8c4a487925c/docs/public/mcp-claude-desktop.avif -------------------------------------------------------------------------------- /docs/public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryoppippi/ccusage/075cb93d208b26213743e3072812d8c4a487925c/docs/public/screenshot.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "allowImportingTsExtensions": false, 5 | "allowJs": true, 6 | "noEmit": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": [ 10 | ".vitepress/**/*", 11 | "**/*.ts", 12 | "**/*.md" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /docs/typedoc.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { globSync } from 'tinyglobby' 3 | 4 | const entryPoints = [ 5 | ...globSync([ 6 | '../src/*.ts', 7 | '!../src/**/*.test.ts', // Exclude test files 8 | '!../src/_*.ts', // Exclude internal files with underscore prefix 9 | ], { 10 | absolute: false, 11 | onlyFiles: true, 12 | }), 13 | '../src/_consts.ts', // Include constants for documentation 14 | ]; 15 | 16 | /** @type {import('typedoc').TypeDocOptions & import('typedoc-plugin-markdown').PluginOptions & { docsRoot?: string } } */ 17 | export default { 18 | // typedoc options 19 | // ref: https://typedoc.org/documents/Options.html 20 | entryPoints, 21 | tsconfig: '../tsconfig.json', 22 | out: 'api', 23 | plugin: ['typedoc-plugin-markdown', 'typedoc-vitepress-theme'], 24 | readme: 'none', 25 | excludeInternal: true, 26 | groupOrder: ['Variables', 'Functions', 'Class'], 27 | categoryOrder: ['*', 'Other'], 28 | sort: ['source-order'], 29 | 30 | // typedoc-plugin-markdown options 31 | // ref: https://typedoc-plugin-markdown.org/docs/options 32 | entryFileName: 'index', 33 | hidePageTitle: false, 34 | useCodeBlocks: true, 35 | disableSources: true, 36 | indexFormat: 'table', 37 | parametersFormat: 'table', 38 | interfacePropertiesFormat: 'table', 39 | classPropertiesFormat: 'table', 40 | propertyMembersFormat: 'table', 41 | typeAliasPropertiesFormat: 'table', 42 | enumMembersFormat: 'table', 43 | 44 | // typedoc-vitepress-theme options 45 | // ref: https://typedoc-plugin-markdown.org/plugins/vitepress/options 46 | docsRoot: '.', 47 | }; 48 | -------------------------------------------------------------------------------- /docs/update-api-index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S bun 2 | 3 | /** 4 | * Post-processing script to update API index with module descriptions 5 | */ 6 | 7 | import { join } from 'node:path'; 8 | import process from 'node:process'; 9 | import { $ } from 'bun'; 10 | 11 | const descriptions = { 12 | '\\_consts': 'Internal constants (not exported in public API)', 13 | 'calculate-cost': 'Cost calculation utilities for usage data analysis', 14 | 'data-loader': 'Data loading utilities for Claude Code usage analysis', 15 | 'debug': 'Debug utilities for cost calculation validation', 16 | 'index': 'Main entry point for ccusage CLI tool', 17 | 'logger': 'Logging utilities for the ccusage application', 18 | 'mcp': 'MCP (Model Context Protocol) server implementation', 19 | 'pricing-fetcher': 'Model pricing data fetcher for cost calculations', 20 | } as const; 21 | 22 | async function updateApiIndex() { 23 | const apiIndexPath = join(process.cwd(), 'api', 'index.md'); 24 | 25 | try { 26 | let content = await Bun.file(apiIndexPath).text(); 27 | 28 | // Replace empty descriptions with actual ones 29 | for (const [module, description] of Object.entries(descriptions)) { 30 | let linkPath = `${module}/index.md`; 31 | // Special case for _consts which links to consts/ 32 | if (module === '\\_consts') { 33 | linkPath = 'consts/index.md'; 34 | } 35 | 36 | const oldPattern = new RegExp(`\\|\\s*\\[${module}\\]\\(${linkPath}\\)\\s*\\|\\s*-\\s*\\|`, 'g'); 37 | content = content.replace(oldPattern, `| [${module}](${linkPath}) | ${description} |`); 38 | } 39 | 40 | await Bun.write(apiIndexPath, content); 41 | console.log('✅ Updated API index with module descriptions'); 42 | } 43 | catch (error) { 44 | console.error('❌ Failed to update API index:', error); 45 | process.exit(1); 46 | } 47 | } 48 | 49 | async function updateConstsPage() { 50 | const constsIndexPath = join(process.cwd(), 'api', 'consts', 'index.md'); 51 | 52 | try { 53 | let content = await Bun.file(constsIndexPath).text(); 54 | 55 | // Add note about constants not being exported (only if not already present) 56 | const noteText = '> **Note**: These constants are internal implementation details and are not exported in the public API. They are documented here for reference purposes only.'; 57 | 58 | if (!content.includes(noteText)) { 59 | const oldHeader = '# \\_consts'; 60 | const newHeader = `# \\_consts 61 | 62 | ${noteText}`; 63 | 64 | content = content.replace(oldHeader, newHeader); 65 | } 66 | 67 | await Bun.write(constsIndexPath, content); 68 | console.log('✅ Updated constants page with disclaimer'); 69 | } 70 | catch (error) { 71 | console.error('❌ Failed to update constants page:', error); 72 | // Don't exit here as this is optional 73 | } 74 | } 75 | 76 | async function main() { 77 | await $`bun typedoc --excludeInternal` 78 | await updateApiIndex(); 79 | await updateConstsPage(); 80 | } 81 | 82 | await main(); 83 | -------------------------------------------------------------------------------- /docs/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/wrangler/config-schema.json", 3 | "name": "ccusage-guide", 4 | "compatibility_date": "2025-07-21", 5 | "build": { 6 | "command": "bun run build" 7 | }, 8 | "assets": { 9 | "directory": ".vitepress/dist/", 10 | "not_found_handling": "404-page" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { ryoppippi } from '@ryoppippi/eslint-config'; 2 | 3 | export default ryoppippi({ 4 | type: 'lib', 5 | svelte: false, 6 | typescript: { 7 | tsconfigPath: './tsconfig.json', 8 | }, 9 | ignores: [ 10 | 'docs/**', 11 | '.lsmcp', 12 | '.claude/settings.local.json', 13 | ], 14 | }, { 15 | rules: { 16 | 'test/no-importing-vitest-globals': 'error', 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ccusage", 3 | "type": "module", 4 | "version": "16.2.0", 5 | "description": "Usage analysis tool for Claude Code", 6 | "author": "ryoppippi", 7 | "license": "MIT", 8 | "funding": "https://github.com/ryoppippi/ccusage?sponsor=1", 9 | "homepage": "https://github.com/ryoppippi/ccusage#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ryoppippi/ccusage.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/ryoppippi/ccusage/issues" 16 | }, 17 | "exports": { 18 | ".": "./dist/index.js", 19 | "./calculate-cost": "./dist/calculate-cost.js", 20 | "./data-loader": "./dist/data-loader.js", 21 | "./debug": "./dist/debug.js", 22 | "./logger": "./dist/logger.js", 23 | "./mcp": "./dist/mcp.js", 24 | "./pricing-fetcher": "./dist/pricing-fetcher.js", 25 | "./package.json": "./package.json" 26 | }, 27 | "main": "./dist/index.js", 28 | "module": "./dist/index.js", 29 | "types": "./dist/index.d.ts", 30 | "bin": "./dist/index.js", 31 | "files": [ 32 | "config-schema.json", 33 | "dist" 34 | ], 35 | "workspaces": [ 36 | ".", 37 | "docs" 38 | ], 39 | "engines": { 40 | "node": ">=20.19.4" 41 | }, 42 | "scripts": { 43 | "build": "bun run generate:schema && tsdown", 44 | "docs:build": "cd docs && bun run build", 45 | "docs:deploy": "cd docs && bun run deploy", 46 | "docs:dev": "cd docs && bun run dev", 47 | "docs:preview": "cd docs && bun run preview", 48 | "format": "bun run lint --fix", 49 | "generate:schema": "bun run scripts/generate-json-schema.ts", 50 | "preinstall": "npx only-allow bun", 51 | "lint": "eslint --cache .", 52 | "mcp": "bunx @modelcontextprotocol/inspector bun run start mcp", 53 | "prepack": "bun run build && bun clean-pkg-json -r scripts.preinstall", 54 | "prepare": "git config --local core.hooksPath .githooks && bun run generate:schema", 55 | "release": "bun lint && bun typecheck && vitest && bun run build && bumpp", 56 | "start": "bun run ./src/index.ts", 57 | "test": "TZ=UTC vitest", 58 | "test:statusline": "cat test/statusline-test.json | bun run start statusline", 59 | "test:statusline:all": "echo 'Testing Sonnet 4:' && bun run test:statusline:sonnet4 && echo 'Testing Opus 4.1:' && bun run test:statusline:opus4 && echo 'Testing Sonnet 4.1:' && bun run test:statusline:sonnet41", 60 | "test:statusline:opus4": "cat test/statusline-test-opus4.json | bun run start statusline --offline", 61 | "test:statusline:sonnet4": "cat test/statusline-test-sonnet4.json | bun run start statusline --offline", 62 | "test:statusline:sonnet41": "cat test/statusline-test-sonnet41.json | bun run start statusline --offline", 63 | "typecheck": "tsgo --noEmit" 64 | }, 65 | "devDependencies": { 66 | "@antfu/utils": "^9.2.0", 67 | "@hono/mcp": "^0.1.0", 68 | "@hono/node-server": "^1.19.0", 69 | "@mizchi/lsmcp": "^0.10.0", 70 | "@modelcontextprotocol/sdk": "^1.17.3", 71 | "@oxc-project/runtime": "^0.82.3", 72 | "@praha/byethrow": "^0.6.3", 73 | "@praha/byethrow-mcp": "^0.1.2", 74 | "@ryoppippi/eslint-config": "^0.3.7", 75 | "@ryoppippi/limo": "npm:@jsr/ryoppippi__limo@^0.2.2", 76 | "@std/async": "npm:@jsr/std__async@^1.0.14", 77 | "@types/bun": "^1.2.20", 78 | "@typescript/native-preview": "^7.0.0-dev.20250821.1", 79 | "ansi-escapes": "^7.0.0", 80 | "bumpp": "^10.2.3", 81 | "clean-pkg-json": "^1.3.0", 82 | "cli-table3": "^0.6.5", 83 | "consola": "^3.4.2", 84 | "es-toolkit": "^1.39.10", 85 | "eslint": "^9.33.0", 86 | "eslint-plugin-format": "^1.0.1", 87 | "fast-sort": "^3.4.1", 88 | "fs-fixture": "^2.8.1", 89 | "get-stdin": "^9.0.0", 90 | "gunshi": "^0.26.3", 91 | "hono": "^4.9.2", 92 | "lint-staged": "^16.1.5", 93 | "nano-spawn": "^1.0.2", 94 | "p-limit": "^7.1.0", 95 | "path-type": "^6.0.0", 96 | "picocolors": "^1.1.1", 97 | "pretty-ms": "^9.2.0", 98 | "publint": "^0.3.12", 99 | "sort-package-json": "^3.4.0", 100 | "string-width": "^7.2.0", 101 | "tinyglobby": "^0.2.14", 102 | "tsdown": "^0.14.1", 103 | "type-fest": "^4.41.0", 104 | "unplugin-macros": "^0.18.0", 105 | "unplugin-unused": "^0.5.2", 106 | "vitest": "^3.2.4", 107 | "xdg-basedir": "^5.1.0", 108 | "zod": "^3.25.67" 109 | }, 110 | "lint-staged": { 111 | "*": [ 112 | "eslint --cache --fix", 113 | "bun run generate:schema", 114 | "bun vitest --passWithNoTests" 115 | ], 116 | "*.{ts,js}": [ 117 | "bun typecheck" 118 | ], 119 | "package.json": [ 120 | "sort-package-json" 121 | ] 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/_consts.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from 'node:os'; 2 | import { xdgConfig } from 'xdg-basedir'; 3 | 4 | /** 5 | * URL for LiteLLM's model pricing and context window data 6 | */ 7 | export const LITELLM_PRICING_URL 8 | = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'; 9 | 10 | /** 11 | * Default number of recent days to include when filtering blocks 12 | * Used in both session blocks and commands for consistent behavior 13 | */ 14 | export const DEFAULT_RECENT_DAYS = 3; 15 | 16 | /** 17 | * Threshold percentage for showing usage warnings in blocks command (80%) 18 | * When usage exceeds this percentage of limits, warnings are displayed 19 | */ 20 | export const BLOCKS_WARNING_THRESHOLD = 0.8; 21 | 22 | /** 23 | * Terminal width threshold for switching to compact display mode in blocks command 24 | * Below this width, tables use more compact formatting 25 | */ 26 | export const BLOCKS_COMPACT_WIDTH_THRESHOLD = 120; 27 | 28 | /** 29 | * Default terminal width when stdout.columns is not available in blocks command 30 | * Used as fallback for responsive table formatting 31 | */ 32 | export const BLOCKS_DEFAULT_TERMINAL_WIDTH = 120; 33 | 34 | /** 35 | * Threshold percentage for considering costs as matching (0.1% tolerance) 36 | * Used in debug cost validation to allow for minor calculation differences 37 | */ 38 | export const DEBUG_MATCH_THRESHOLD_PERCENT = 0.1; 39 | 40 | /** 41 | * User's home directory path 42 | * Centralized access to OS home directory for consistent path building 43 | */ 44 | export const USER_HOME_DIR = homedir(); 45 | 46 | /** 47 | * XDG config directory path 48 | * Uses XDG_CONFIG_HOME if set, otherwise falls back to ~/.config 49 | */ 50 | const XDG_CONFIG_DIR = xdgConfig ?? `${USER_HOME_DIR}/.config`; 51 | 52 | /** 53 | * Default Claude data directory path (~/.claude) 54 | * Used as base path for loading usage data from JSONL files 55 | */ 56 | export const DEFAULT_CLAUDE_CODE_PATH = '.claude'; 57 | 58 | /** 59 | * Default Claude data directory path using XDG config directory 60 | * Uses XDG_CONFIG_HOME if set, otherwise falls back to ~/.config/claude 61 | */ 62 | export const DEFAULT_CLAUDE_CONFIG_PATH = `${XDG_CONFIG_DIR}/claude`; 63 | 64 | /** 65 | * Environment variable for specifying multiple Claude data directories 66 | * Supports comma-separated paths for multiple locations 67 | */ 68 | export const CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR'; 69 | 70 | /** 71 | * Claude projects directory name within the data directory 72 | * Contains subdirectories for each project with usage data 73 | */ 74 | export const CLAUDE_PROJECTS_DIR_NAME = 'projects'; 75 | 76 | /** 77 | * JSONL file glob pattern for finding usage data files 78 | * Used to recursively find all JSONL files in project directories 79 | */ 80 | export const USAGE_DATA_GLOB_PATTERN = '**/*.jsonl'; 81 | 82 | /** 83 | * Default port for MCP server HTTP transport 84 | * Used when no port is specified for MCP server communication 85 | */ 86 | export const MCP_DEFAULT_PORT = 8080; 87 | 88 | /** 89 | * Default refresh interval in seconds for live monitoring mode 90 | * Used in blocks command for real-time updates 91 | */ 92 | export const DEFAULT_REFRESH_INTERVAL_SECONDS = 1; 93 | 94 | /** 95 | * Minimum refresh interval in seconds for live monitoring mode 96 | * Prevents too-frequent updates that could impact performance 97 | */ 98 | export const MIN_REFRESH_INTERVAL_SECONDS = 1; 99 | 100 | /** 101 | * Maximum refresh interval in seconds for live monitoring mode 102 | * Prevents too-slow updates that reduce monitoring effectiveness 103 | */ 104 | export const MAX_REFRESH_INTERVAL_SECONDS = 60; 105 | 106 | /** 107 | * Frame rate limit for live monitoring (16ms = ~60fps) 108 | * Prevents terminal flickering and excessive CPU usage during rapid updates 109 | */ 110 | export const MIN_RENDER_INTERVAL_MS = 16; 111 | 112 | /** 113 | * Burn rate thresholds for indicator display (tokens per minute) 114 | */ 115 | export const BURN_RATE_THRESHOLDS = { 116 | HIGH: 1000, 117 | MODERATE: 500, 118 | } as const; 119 | 120 | /** 121 | * Context usage percentage thresholds for color coding 122 | */ 123 | export const DEFAULT_CONTEXT_USAGE_THRESHOLDS = { 124 | LOW: 50, // Below 50% - green 125 | MEDIUM: 80, // 50-80% - yellow 126 | // Above 80% - red 127 | } as const; 128 | 129 | /** 130 | * Days of the week for weekly aggregation 131 | */ 132 | export const WEEK_DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] as const; 133 | 134 | /** 135 | * Week day names type 136 | */ 137 | export type WeekDay = typeof WEEK_DAYS[number]; 138 | 139 | /** 140 | * Day of week as number (0 = Sunday, 1 = Monday, ..., 6 = Saturday) 141 | */ 142 | export type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6; 143 | 144 | /** 145 | * Default configuration file name for storing usage data 146 | * Used to load and save configuration settings 147 | */ 148 | export const CONFIG_FILE_NAME = 'ccusage.json'; 149 | 150 | /** 151 | * Default locale for date formatting (en-CA provides YYYY-MM-DD ISO format) 152 | * Used consistently across the application for date parsing and display 153 | */ 154 | export const DEFAULT_LOCALE = 'en-CA'; 155 | -------------------------------------------------------------------------------- /src/_daily-grouping.ts: -------------------------------------------------------------------------------- 1 | import type { DailyProjectOutput } from './_json-output-types.ts'; 2 | import type { loadDailyUsageData } from './data-loader.ts'; 3 | import { createDailyDate, createModelName } from './_types.ts'; 4 | import { getTotalTokens } from './calculate-cost.ts'; 5 | 6 | /** 7 | * Type for daily data returned from loadDailyUsageData 8 | */ 9 | type DailyData = Awaited>; 10 | 11 | /** 12 | * Group daily usage data by project for JSON output 13 | */ 14 | export function groupByProject(dailyData: DailyData): Record { 15 | const projects: Record = {}; 16 | 17 | for (const data of dailyData) { 18 | const projectName = data.project ?? 'unknown'; 19 | 20 | if (projects[projectName] == null) { 21 | projects[projectName] = []; 22 | } 23 | 24 | projects[projectName].push({ 25 | date: data.date, 26 | inputTokens: data.inputTokens, 27 | outputTokens: data.outputTokens, 28 | cacheCreationTokens: data.cacheCreationTokens, 29 | cacheReadTokens: data.cacheReadTokens, 30 | totalTokens: getTotalTokens(data), 31 | totalCost: data.totalCost, 32 | modelsUsed: data.modelsUsed, 33 | modelBreakdowns: data.modelBreakdowns, 34 | }); 35 | } 36 | 37 | return projects; 38 | } 39 | 40 | /** 41 | * Group daily usage data by project for table display 42 | */ 43 | export function groupDataByProject(dailyData: DailyData): Record { 44 | const projects: Record = {}; 45 | 46 | for (const data of dailyData) { 47 | const projectName = data.project ?? 'unknown'; 48 | 49 | if (projects[projectName] == null) { 50 | projects[projectName] = []; 51 | } 52 | 53 | projects[projectName].push(data); 54 | } 55 | 56 | return projects; 57 | } 58 | 59 | if (import.meta.vitest != null) { 60 | describe('groupByProject', () => { 61 | it('groups daily data by project for JSON output', () => { 62 | const mockData = [ 63 | { 64 | date: createDailyDate('2024-01-01'), 65 | project: 'project-a', 66 | inputTokens: 1000, 67 | outputTokens: 500, 68 | cacheCreationTokens: 100, 69 | cacheReadTokens: 200, 70 | totalCost: 0.01, 71 | modelsUsed: [createModelName('claude-sonnet-4-20250514')], 72 | modelBreakdowns: [], 73 | }, 74 | { 75 | date: createDailyDate('2024-01-01'), 76 | project: 'project-b', 77 | inputTokens: 2000, 78 | outputTokens: 1000, 79 | cacheCreationTokens: 200, 80 | cacheReadTokens: 300, 81 | totalCost: 0.02, 82 | modelsUsed: [createModelName('claude-opus-4-20250514')], 83 | modelBreakdowns: [], 84 | }, 85 | ]; 86 | 87 | const result = groupByProject(mockData); 88 | 89 | expect(Object.keys(result)).toHaveLength(2); 90 | expect(result['project-a']).toHaveLength(1); 91 | expect(result['project-b']).toHaveLength(1); 92 | expect(result['project-a']![0]!.totalTokens).toBe(1800); 93 | expect(result['project-b']![0]!.totalTokens).toBe(3500); 94 | }); 95 | 96 | it('handles unknown project names', () => { 97 | const mockData = [ 98 | { 99 | date: createDailyDate('2024-01-01'), 100 | project: undefined, 101 | inputTokens: 1000, 102 | outputTokens: 500, 103 | cacheCreationTokens: 0, 104 | cacheReadTokens: 0, 105 | totalCost: 0.01, 106 | modelsUsed: [createModelName('claude-sonnet-4-20250514')], 107 | modelBreakdowns: [], 108 | }, 109 | ]; 110 | 111 | const result = groupByProject(mockData); 112 | 113 | expect(Object.keys(result)).toHaveLength(1); 114 | expect(result.unknown).toHaveLength(1); 115 | }); 116 | }); 117 | 118 | describe('groupDataByProject', () => { 119 | it('groups daily data by project for table display', () => { 120 | const mockData = [ 121 | { 122 | date: createDailyDate('2024-01-01'), 123 | project: 'project-a', 124 | inputTokens: 1000, 125 | outputTokens: 500, 126 | cacheCreationTokens: 100, 127 | cacheReadTokens: 200, 128 | totalCost: 0.01, 129 | modelsUsed: [createModelName('claude-sonnet-4-20250514')], 130 | modelBreakdowns: [], 131 | }, 132 | { 133 | date: createDailyDate('2024-01-02'), 134 | project: 'project-a', 135 | inputTokens: 800, 136 | outputTokens: 400, 137 | cacheCreationTokens: 50, 138 | cacheReadTokens: 150, 139 | totalCost: 0.008, 140 | modelsUsed: [createModelName('claude-sonnet-4-20250514')], 141 | modelBreakdowns: [], 142 | }, 143 | ]; 144 | 145 | const result = groupDataByProject(mockData); 146 | 147 | expect(Object.keys(result)).toHaveLength(1); 148 | expect(result['project-a']).toHaveLength(2); 149 | expect(result['project-a']).toEqual(mockData); 150 | }); 151 | }); 152 | } 153 | -------------------------------------------------------------------------------- /src/_jq-processor.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@praha/byethrow'; 2 | import spawn from 'nano-spawn'; 3 | 4 | /** 5 | * Process JSON data with a jq command 6 | * @param jsonData - The JSON data to process 7 | * @param jqCommand - The jq command/filter to apply 8 | * @returns The processed output from jq 9 | */ 10 | export async function processWithJq(jsonData: unknown, jqCommand: string): Result.ResultAsync { 11 | // Convert JSON data to string 12 | const jsonString = JSON.stringify(jsonData); 13 | 14 | // Use Result.try with object form to wrap spawn call 15 | const result = Result.try({ 16 | try: async () => { 17 | const spawnResult = await spawn('jq', [jqCommand], { 18 | stdin: { string: jsonString }, 19 | }); 20 | return spawnResult.output.trim(); 21 | }, 22 | catch: (error: unknown) => { 23 | if (error instanceof Error) { 24 | // Check if jq is not installed 25 | if (error.message.includes('ENOENT') || error.message.includes('not found')) { 26 | return new Error('jq command not found. Please install jq to use the --jq option.'); 27 | } 28 | // Return other errors (e.g., invalid jq syntax) 29 | return new Error(`jq processing failed: ${error.message}`); 30 | } 31 | return new Error('Unknown error during jq processing'); 32 | }, 33 | }); 34 | 35 | return result(); 36 | } 37 | 38 | // In-source tests 39 | if (import.meta.vitest != null) { 40 | describe('processWithJq', () => { 41 | it('should process JSON with simple filter', async () => { 42 | const data = { name: 'test', value: 42 }; 43 | const result = await processWithJq(data, '.name'); 44 | const unwrapped = Result.unwrap(result); 45 | expect(unwrapped).toBe('"test"'); 46 | }); 47 | 48 | it('should process JSON with complex filter', async () => { 49 | const data = { 50 | items: [ 51 | { id: 1, name: 'apple' }, 52 | { id: 2, name: 'banana' }, 53 | ], 54 | }; 55 | const result = await processWithJq(data, '.items | map(.name)'); 56 | const unwrapped = Result.unwrap(result); 57 | const parsed = JSON.parse(unwrapped) as string[]; 58 | expect(parsed).toEqual(['apple', 'banana']); 59 | }); 60 | 61 | it('should handle raw output', async () => { 62 | const data = { message: 'hello world' }; 63 | const result = await processWithJq(data, '.message | @text'); 64 | const unwrapped = Result.unwrap(result); 65 | expect(unwrapped).toBe('"hello world"'); 66 | }); 67 | 68 | it('should return error for invalid jq syntax', async () => { 69 | const data = { test: 'value' }; 70 | const result = await processWithJq(data, 'invalid syntax {'); 71 | const error = Result.unwrapError(result); 72 | expect(error.message).toContain('jq processing failed'); 73 | }); 74 | 75 | it('should handle complex jq operations', async () => { 76 | const data = { 77 | users: [ 78 | { name: 'Alice', age: 30 }, 79 | { name: 'Bob', age: 25 }, 80 | { name: 'Charlie', age: 35 }, 81 | ], 82 | }; 83 | const result = await processWithJq(data, '.users | sort_by(.age) | .[0].name'); 84 | const unwrapped = Result.unwrap(result); 85 | expect(unwrapped).toBe('"Bob"'); 86 | }); 87 | 88 | it('should handle numeric output', async () => { 89 | const data = { values: [1, 2, 3, 4, 5] }; 90 | const result = await processWithJq(data, '.values | add'); 91 | const unwrapped = Result.unwrap(result); 92 | expect(unwrapped).toBe('15'); 93 | }); 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /src/_json-output-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview JSON output interface types for daily command groupByProject function 3 | * 4 | * This module provides TypeScript interfaces for the JSON output structure 5 | * used by the daily command's groupByProject function, replacing the 6 | * unsafe Record type with proper type definitions. 7 | * 8 | * @module json-output-types 9 | */ 10 | 11 | import type { DailyDate, ModelName } from './_types.ts'; 12 | import type { ModelBreakdown } from './data-loader.ts'; 13 | 14 | /** 15 | * Interface for daily command JSON output structure (groupByProject) 16 | * Used in src/commands/daily.ts 17 | */ 18 | export type DailyProjectOutput = { 19 | date: DailyDate; 20 | inputTokens: number; 21 | outputTokens: number; 22 | cacheCreationTokens: number; 23 | cacheReadTokens: number; 24 | totalTokens: number; 25 | totalCost: number; 26 | modelsUsed: ModelName[]; 27 | modelBreakdowns: ModelBreakdown[]; 28 | }; 29 | -------------------------------------------------------------------------------- /src/_macro.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prefetch claude data for the current user. 3 | */ 4 | 5 | import type { ModelPricing } from './_types.ts'; 6 | import { LITELLM_PRICING_URL } from './_consts.ts'; 7 | import { modelPricingSchema } from './_types.ts'; 8 | 9 | /** 10 | * Prefetches the pricing data for Claude models from the LiteLLM API. 11 | * This function fetches the pricing data and filters out models that start with 'claude-'. 12 | * It returns a record of model names to their pricing information. 13 | * 14 | * @returns A promise that resolves to a record of model names and their pricing information. 15 | * @throws Will throw an error if the fetch operation fails. 16 | */ 17 | export async function prefetchClaudePricing(): Promise> { 18 | const response = await fetch(LITELLM_PRICING_URL); 19 | if (!response.ok) { 20 | throw new Error(`Failed to fetch pricing data: ${response.statusText}`); 21 | } 22 | 23 | const data = await response.json() as Record; 24 | 25 | const prefetchClaudeData: Record = {}; 26 | 27 | // Cache all models that start with 'claude-' 28 | for (const [modelName, modelData] of Object.entries(data)) { 29 | if (modelName.startsWith('claude-') && modelData != null && typeof modelData === 'object') { 30 | const parsed = modelPricingSchema.safeParse(modelData); 31 | if (parsed.success) { 32 | prefetchClaudeData[modelName] = parsed.data; 33 | } 34 | } 35 | } 36 | 37 | return prefetchClaudeData; 38 | } 39 | -------------------------------------------------------------------------------- /src/_project-names.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Project name formatting and alias utilities 3 | * 4 | * Provides utilities for formatting raw project directory names into user-friendly 5 | * display names with support for custom aliases and improved path parsing. 6 | * 7 | * @module project-names 8 | */ 9 | 10 | /** 11 | * Extract meaningful project name from directory-style project paths 12 | * Uses improved heuristics to handle complex project structures 13 | * 14 | * @param projectName - Raw project name from directory path 15 | * @returns Cleaned and formatted project name 16 | * 17 | * @example 18 | * ```typescript 19 | * // Basic cleanup 20 | * parseProjectName('-Users-phaedrus-Development-ccusage') 21 | * // → 'ccusage' 22 | * 23 | * // Complex project with feature branch 24 | * parseProjectName('-Users-phaedrus-Development-adminifi-edugakko-api--feature-ticket-002-configure-dependabot') 25 | * // → 'configure-dependabot' 26 | * 27 | * // Handle unknown projects 28 | * parseProjectName('unknown') 29 | * // → 'Unknown Project' 30 | * ``` 31 | */ 32 | function parseProjectName(projectName: string): string { 33 | if (projectName === 'unknown' || projectName === '') { 34 | return 'Unknown Project'; 35 | } 36 | 37 | // Remove common directory prefixes 38 | let cleaned = projectName; 39 | 40 | // Handle Windows-style paths: C:\Users\... or \Users\... 41 | if (cleaned.match(/^[A-Z]:\\Users\\|^\\Users\\/) != null) { 42 | const segments = cleaned.split('\\'); 43 | const userIndex = segments.findIndex(seg => seg === 'Users'); 44 | if (userIndex !== -1 && userIndex + 3 < segments.length) { 45 | // Take everything after Users/username/Projects or similar 46 | cleaned = segments.slice(userIndex + 3).join('-'); 47 | } 48 | } 49 | 50 | // Handle Unix-style paths: /Users/... or -Users-... 51 | if (cleaned.startsWith('-Users-') || cleaned.startsWith('/Users/')) { 52 | const separator = cleaned.startsWith('-Users-') ? '-' : '/'; 53 | const segments = cleaned.split(separator).filter(s => s.length > 0); 54 | const userIndex = segments.findIndex(seg => seg === 'Users'); 55 | 56 | if (userIndex !== -1 && userIndex + 3 < segments.length) { 57 | // Take everything after Users/username/Development or similar 58 | cleaned = segments.slice(userIndex + 3).join('-'); 59 | } 60 | } 61 | 62 | // If no path cleanup occurred, use original name 63 | if (cleaned === projectName) { 64 | // Just basic cleanup for non-path names 65 | cleaned = projectName.replace(/^[/\\-]+|[/\\-]+$/g, ''); 66 | } 67 | 68 | // Handle UUID-like patterns 69 | if (cleaned.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i) != null) { 70 | // Extract last two segments of UUID for brevity 71 | const parts = cleaned.split('-'); 72 | if (parts.length >= 5) { 73 | // Take the last two segments, which may include file extension in the last segment 74 | cleaned = parts.slice(-2).join('-'); 75 | } 76 | } 77 | 78 | // Improved project name extraction for complex names 79 | if (cleaned.includes('--')) { 80 | // Handle project--feature patterns like "adminifi-edugakko-api--feature-ticket-002" 81 | const parts = cleaned.split('--'); 82 | if (parts.length >= 2 && parts[0] != null) { 83 | // Take the main project part before the first -- 84 | cleaned = parts[0]; 85 | } 86 | } 87 | 88 | // For compound project names, try to extract the most meaningful part 89 | if (cleaned.includes('-') && cleaned.length > 20) { 90 | const segments = cleaned.split('-'); 91 | 92 | // Look for common meaningful patterns 93 | const meaningfulSegments = segments.filter(seg => 94 | seg.length > 2 95 | && seg.match(/^(?:dev|development|feat|feature|fix|bug|test|staging|prod|production|main|master|branch)$/i) == null, 96 | ); 97 | 98 | // If we have compound project names like "adminifi-edugakko-api" 99 | // Try to find the last 2-3 meaningful segments 100 | if (meaningfulSegments.length >= 2) { 101 | // Take last 2-3 segments to get "edugakko-api" from "adminifi-edugakko-api" 102 | const lastSegments = meaningfulSegments.slice(-2); 103 | if (lastSegments.join('-').length >= 6) { 104 | cleaned = lastSegments.join('-'); 105 | } 106 | else if (meaningfulSegments.length >= 3) { 107 | cleaned = meaningfulSegments.slice(-3).join('-'); 108 | } 109 | } 110 | } 111 | 112 | // Final cleanup 113 | cleaned = cleaned.replace(/^[/\\-]+|[/\\-]+$/g, ''); 114 | 115 | return cleaned !== '' ? cleaned : (projectName !== '' ? projectName : 'Unknown Project'); 116 | } 117 | 118 | /** 119 | * Format project name for display with custom alias support 120 | * 121 | * @param projectName - Raw project name from directory path 122 | * @param aliases - Optional map of project names to their aliases 123 | * @returns User-friendly project name with alias support 124 | * 125 | * @example 126 | * ```typescript 127 | * // Without aliases 128 | * formatProjectName('-Users-phaedrus-Development-ccusage') 129 | * // → 'ccusage' 130 | * 131 | * // With alias 132 | * const aliases = new Map([['ccusage', 'Usage Tracker']]); 133 | * formatProjectName('-Users-phaedrus-Development-ccusage', aliases) 134 | * // → 'Usage Tracker' 135 | * ``` 136 | */ 137 | export function formatProjectName(projectName: string, aliases?: Map): string { 138 | // Check for custom alias first 139 | if (aliases != null && aliases.has(projectName)) { 140 | return aliases.get(projectName)!; 141 | } 142 | 143 | // Parse the project name using improved logic 144 | const parsed = parseProjectName(projectName); 145 | 146 | // Check if parsed name has an alias 147 | if (aliases != null && aliases.has(parsed)) { 148 | return aliases.get(parsed)!; 149 | } 150 | 151 | return parsed; 152 | } 153 | 154 | if (import.meta.vitest != null) { 155 | const { describe, it, expect } = import.meta.vitest; 156 | 157 | describe('project name formatting', () => { 158 | describe('parseProjectName', () => { 159 | it('handles unknown project names', () => { 160 | expect(formatProjectName('unknown')).toBe('Unknown Project'); 161 | expect(formatProjectName('')).toBe('Unknown Project'); 162 | }); 163 | 164 | it('extracts project names from Unix-style paths', () => { 165 | expect(formatProjectName('-Users-phaedrus-Development-ccusage')).toBe('ccusage'); 166 | expect(formatProjectName('/Users/phaedrus/Development/ccusage')).toBe('ccusage'); 167 | }); 168 | 169 | it('handles complex project names with features', () => { 170 | const complexName = '-Users-phaedrus-Development-adminifi-edugakko-api--feature-ticket-002-configure-dependabot'; 171 | const result = formatProjectName(complexName); 172 | // Current logic processes the name and extracts meaningful segments 173 | expect(result).toBe('configure-dependabot'); 174 | }); 175 | 176 | it('handles UUID-based project names', () => { 177 | const uuidName = 'a2cd99ed-a586-4fe4-8f59-b0026409ec09.jsonl'; 178 | const result = formatProjectName(uuidName); 179 | expect(result).toBe('8f59-b0026409ec09.jsonl'); 180 | }); 181 | 182 | it('returns original name for simple names', () => { 183 | expect(formatProjectName('simple-project')).toBe('simple-project'); 184 | expect(formatProjectName('project')).toBe('project'); 185 | }); 186 | }); 187 | 188 | describe('custom aliases', () => { 189 | it('uses configured aliases', () => { 190 | const aliases = new Map([ 191 | ['ccusage', 'Usage Tracker'], 192 | ['test', 'Test Project'], 193 | ]); 194 | 195 | expect(formatProjectName('ccusage', aliases)).toBe('Usage Tracker'); 196 | expect(formatProjectName('test', aliases)).toBe('Test Project'); 197 | expect(formatProjectName('other', aliases)).toBe('other'); 198 | }); 199 | 200 | it('applies aliases to parsed project names', () => { 201 | const aliases = new Map([['ccusage', 'Usage Tracker']]); 202 | 203 | expect(formatProjectName('-Users-phaedrus-Development-ccusage', aliases)).toBe('Usage Tracker'); 204 | }); 205 | 206 | it('works without aliases', () => { 207 | expect(formatProjectName('test')).toBe('test'); 208 | expect(formatProjectName('test', undefined)).toBe('test'); 209 | expect(formatProjectName('test', new Map())).toBe('test'); 210 | }); 211 | }); 212 | }); 213 | } 214 | -------------------------------------------------------------------------------- /src/_shared-args.ts: -------------------------------------------------------------------------------- 1 | import type { Args } from 'gunshi'; 2 | import type { CostMode, SortOrder } from './_types.ts'; 3 | import { DEFAULT_LOCALE } from './_consts.ts'; 4 | import { CostModes, filterDateSchema, SortOrders } from './_types.ts'; 5 | 6 | /** 7 | * Parses and validates a date argument in YYYYMMDD format 8 | * @param value - Date string to parse 9 | * @returns Validated date string 10 | * @throws TypeError if date format is invalid 11 | */ 12 | function parseDateArg(value: string): string { 13 | const result = filterDateSchema.safeParse(value); 14 | if (!result.success) { 15 | throw new TypeError(result.error.issues[0]?.message ?? 'Invalid date format'); 16 | } 17 | return result.data; 18 | } 19 | 20 | /** 21 | * Shared command line arguments used across multiple CLI commands 22 | */ 23 | export const sharedArgs = { 24 | since: { 25 | type: 'custom', 26 | short: 's', 27 | description: 'Filter from date (YYYYMMDD format)', 28 | parse: parseDateArg, 29 | }, 30 | until: { 31 | type: 'custom', 32 | short: 'u', 33 | description: 'Filter until date (YYYYMMDD format)', 34 | parse: parseDateArg, 35 | }, 36 | json: { 37 | type: 'boolean', 38 | short: 'j', 39 | description: 'Output in JSON format', 40 | default: false, 41 | }, 42 | mode: { 43 | type: 'enum', 44 | short: 'm', 45 | description: 46 | 'Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)', 47 | default: 'auto' as const satisfies CostMode, 48 | choices: CostModes, 49 | }, 50 | debug: { 51 | type: 'boolean', 52 | short: 'd', 53 | description: 'Show pricing mismatch information for debugging', 54 | default: false, 55 | }, 56 | debugSamples: { 57 | type: 'number', 58 | description: 59 | 'Number of sample discrepancies to show in debug output (default: 5)', 60 | default: 5, 61 | }, 62 | order: { 63 | type: 'enum', 64 | short: 'o', 65 | description: 'Sort order: desc (newest first) or asc (oldest first)', 66 | default: 'asc' as const satisfies SortOrder, 67 | choices: SortOrders, 68 | }, 69 | breakdown: { 70 | type: 'boolean', 71 | short: 'b', 72 | description: 'Show per-model cost breakdown', 73 | default: false, 74 | }, 75 | offline: { 76 | type: 'boolean', 77 | negatable: true, 78 | short: 'O', 79 | description: 'Use cached pricing data for Claude models instead of fetching from API', 80 | default: false, 81 | }, 82 | color: { // --color and FORCE_COLOR=1 is handled by picocolors 83 | type: 'boolean', 84 | description: 'Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.', 85 | }, 86 | noColor: { // --no-color and NO_COLOR=1 is handled by picocolors 87 | type: 'boolean', 88 | description: 'Disable colored output (default: auto). NO_COLOR=1 has the same effect.', 89 | }, 90 | timezone: { 91 | type: 'string', 92 | short: 'z', 93 | description: 'Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone', 94 | }, 95 | locale: { 96 | type: 'string', 97 | short: 'l', 98 | description: 'Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)', 99 | default: DEFAULT_LOCALE, 100 | }, 101 | jq: { 102 | type: 'string', 103 | short: 'q', 104 | description: 'Process JSON output with jq command (requires jq binary, implies --json)', 105 | }, 106 | config: { 107 | type: 'string', 108 | description: 'Path to configuration file (default: auto-discovery)', 109 | }, 110 | compact: { 111 | type: 'boolean', 112 | description: 'Force compact mode for narrow displays (better for screenshots)', 113 | default: false, 114 | }, 115 | } as const satisfies Args; 116 | 117 | /** 118 | * Shared command configuration for Gunshi CLI commands 119 | */ 120 | export const sharedCommandConfig = { 121 | args: sharedArgs, 122 | toKebab: true, 123 | } as const; 124 | -------------------------------------------------------------------------------- /src/_token-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Token calculation utilities 3 | * 4 | * This module provides shared utilities for calculating token totals 5 | * across different token types. Used throughout the application to 6 | * ensure consistent token counting logic. 7 | */ 8 | 9 | /** 10 | * Token counts structure for raw usage data (uses InputTokens suffix) 11 | */ 12 | export type TokenCounts = { 13 | inputTokens: number; 14 | outputTokens: number; 15 | cacheCreationInputTokens: number; 16 | cacheReadInputTokens: number; 17 | }; 18 | 19 | /** 20 | * Token counts structure for aggregated data (uses shorter names) 21 | */ 22 | export type AggregatedTokenCounts = { 23 | inputTokens: number; 24 | outputTokens: number; 25 | cacheCreationTokens: number; 26 | cacheReadTokens: number; 27 | }; 28 | 29 | /** 30 | * Union type that supports both token count formats 31 | */ 32 | export type AnyTokenCounts = TokenCounts | AggregatedTokenCounts; 33 | 34 | /** 35 | * Calculates the total number of tokens across all token types 36 | * Supports both raw usage data format and aggregated data format 37 | * @param tokenCounts - Object containing counts for each token type 38 | * @returns Total number of tokens 39 | */ 40 | export function getTotalTokens(tokenCounts: AnyTokenCounts): number { 41 | // Support both property naming conventions 42 | const cacheCreation = 'cacheCreationInputTokens' in tokenCounts 43 | ? tokenCounts.cacheCreationInputTokens 44 | : (tokenCounts).cacheCreationTokens; 45 | 46 | const cacheRead = 'cacheReadInputTokens' in tokenCounts 47 | ? tokenCounts.cacheReadInputTokens 48 | : (tokenCounts).cacheReadTokens; 49 | 50 | return ( 51 | tokenCounts.inputTokens 52 | + tokenCounts.outputTokens 53 | + cacheCreation 54 | + cacheRead 55 | ); 56 | } 57 | 58 | // In-source testing 59 | if (import.meta.vitest != null) { 60 | describe('getTotalTokens', () => { 61 | it('should sum all token types correctly (raw format)', () => { 62 | const tokens: TokenCounts = { 63 | inputTokens: 1000, 64 | outputTokens: 500, 65 | cacheCreationInputTokens: 2000, 66 | cacheReadInputTokens: 300, 67 | }; 68 | expect(getTotalTokens(tokens)).toBe(3800); 69 | }); 70 | 71 | it('should sum all token types correctly (aggregated format)', () => { 72 | const tokens: AggregatedTokenCounts = { 73 | inputTokens: 1000, 74 | outputTokens: 500, 75 | cacheCreationTokens: 2000, 76 | cacheReadTokens: 300, 77 | }; 78 | expect(getTotalTokens(tokens)).toBe(3800); 79 | }); 80 | 81 | it('should handle zero values (raw format)', () => { 82 | const tokens: TokenCounts = { 83 | inputTokens: 0, 84 | outputTokens: 0, 85 | cacheCreationInputTokens: 0, 86 | cacheReadInputTokens: 0, 87 | }; 88 | expect(getTotalTokens(tokens)).toBe(0); 89 | }); 90 | 91 | it('should handle zero values (aggregated format)', () => { 92 | const tokens: AggregatedTokenCounts = { 93 | inputTokens: 0, 94 | outputTokens: 0, 95 | cacheCreationTokens: 0, 96 | cacheReadTokens: 0, 97 | }; 98 | expect(getTotalTokens(tokens)).toBe(0); 99 | }); 100 | 101 | it('should handle missing cache tokens (raw format)', () => { 102 | const tokens: TokenCounts = { 103 | inputTokens: 1000, 104 | outputTokens: 500, 105 | cacheCreationInputTokens: 0, 106 | cacheReadInputTokens: 0, 107 | }; 108 | expect(getTotalTokens(tokens)).toBe(1500); 109 | }); 110 | 111 | it('should handle missing cache tokens (aggregated format)', () => { 112 | const tokens: AggregatedTokenCounts = { 113 | inputTokens: 1000, 114 | outputTokens: 500, 115 | cacheCreationTokens: 0, 116 | cacheReadTokens: 0, 117 | }; 118 | expect(getTotalTokens(tokens)).toBe(1500); 119 | }); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /src/_types.ts: -------------------------------------------------------------------------------- 1 | import type { TupleToUnion } from 'type-fest'; 2 | import { z } from 'zod'; 3 | 4 | /** 5 | * Branded Zod schemas for type safety using Zod's built-in brand functionality 6 | */ 7 | 8 | // Core identifier schemas 9 | export const modelNameSchema = z.string() 10 | .min(1, 'Model name cannot be empty') 11 | .brand<'ModelName'>(); 12 | 13 | export const sessionIdSchema = z.string() 14 | .min(1, 'Session ID cannot be empty') 15 | .brand<'SessionId'>(); 16 | 17 | export const requestIdSchema = z.string() 18 | .min(1, 'Request ID cannot be empty') 19 | .brand<'RequestId'>(); 20 | 21 | export const messageIdSchema = z.string() 22 | .min(1, 'Message ID cannot be empty') 23 | .brand<'MessageId'>(); 24 | 25 | // Date and timestamp schemas 26 | export const isoTimestampSchema = z.string() 27 | .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/, 'Invalid ISO timestamp') 28 | .brand<'ISOTimestamp'>(); 29 | 30 | export const dailyDateSchema = z.string() 31 | .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format') 32 | .brand<'DailyDate'>(); 33 | 34 | export const activityDateSchema = z.string() 35 | .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format') 36 | .brand<'ActivityDate'>(); 37 | 38 | export const monthlyDateSchema = z.string() 39 | .regex(/^\d{4}-\d{2}$/, 'Date must be in YYYY-MM format') 40 | .brand<'MonthlyDate'>(); 41 | 42 | export const weeklyDateSchema = z 43 | .string() 44 | .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format') 45 | .brand<'WeeklyDate'>(); 46 | 47 | export const filterDateSchema = z.string() 48 | .regex(/^\d{8}$/, 'Date must be in YYYYMMDD format') 49 | .brand<'FilterDate'>(); 50 | 51 | // Other domain-specific schemas 52 | export const projectPathSchema = z.string() 53 | .min(1, 'Project path cannot be empty') 54 | .brand<'ProjectPath'>(); 55 | 56 | export const versionSchema = z.string() 57 | .regex(/^\d+\.\d+\.\d+/, 'Invalid version format') 58 | .brand<'Version'>(); 59 | 60 | /** 61 | * Inferred branded types from schemas 62 | */ 63 | export type ModelName = z.infer; 64 | export type SessionId = z.infer; 65 | export type RequestId = z.infer; 66 | export type MessageId = z.infer; 67 | export type ISOTimestamp = z.infer; 68 | export type DailyDate = z.infer; 69 | export type ActivityDate = z.infer; 70 | export type MonthlyDate = z.infer; 71 | export type WeeklyDate = z.infer; 72 | export type Bucket = MonthlyDate | WeeklyDate; 73 | export type FilterDate = z.infer; 74 | export type ProjectPath = z.infer; 75 | export type Version = z.infer; 76 | 77 | /** 78 | * Helper functions to create branded values by parsing and validating input strings 79 | * These functions should be used when converting plain strings to branded types 80 | */ 81 | export const createModelName = (value: string): ModelName => modelNameSchema.parse(value); 82 | export const createSessionId = (value: string): SessionId => sessionIdSchema.parse(value); 83 | export const createRequestId = (value: string): RequestId => requestIdSchema.parse(value); 84 | export const createMessageId = (value: string): MessageId => messageIdSchema.parse(value); 85 | export const createISOTimestamp = (value: string): ISOTimestamp => isoTimestampSchema.parse(value); 86 | export const createDailyDate = (value: string): DailyDate => dailyDateSchema.parse(value); 87 | export const createActivityDate = (value: string): ActivityDate => activityDateSchema.parse(value); 88 | export const createMonthlyDate = (value: string): MonthlyDate => monthlyDateSchema.parse(value); 89 | export const createWeeklyDate = (value: string): WeeklyDate => weeklyDateSchema.parse(value); 90 | export const createFilterDate = (value: string): FilterDate => filterDateSchema.parse(value); 91 | export const createProjectPath = (value: string): ProjectPath => projectPathSchema.parse(value); 92 | export const createVersion = (value: string): Version => versionSchema.parse(value); 93 | 94 | export function createBucket(value: string): Bucket { 95 | if (weeklyDateSchema.safeParse(value).success) { 96 | return createWeeklyDate(value); 97 | } 98 | return createMonthlyDate(value); 99 | }; 100 | 101 | /** 102 | * Available cost calculation modes 103 | * - auto: Use pre-calculated costs when available, otherwise calculate from tokens 104 | * - calculate: Always calculate costs from token counts using model pricing 105 | * - display: Always use pre-calculated costs, show 0 for missing costs 106 | */ 107 | export const CostModes = ['auto', 'calculate', 'display'] as const; 108 | 109 | /** 110 | * Union type for cost calculation modes 111 | */ 112 | export type CostMode = TupleToUnion; 113 | 114 | /** 115 | * Available sort orders for data presentation 116 | */ 117 | export const SortOrders = ['desc', 'asc'] as const; 118 | 119 | /** 120 | * Union type for sort order options 121 | */ 122 | export type SortOrder = TupleToUnion; 123 | 124 | /** 125 | * Zod schema for model pricing information from LiteLLM 126 | */ 127 | export const modelPricingSchema = z.object({ 128 | input_cost_per_token: z.number().optional(), 129 | output_cost_per_token: z.number().optional(), 130 | cache_creation_input_token_cost: z.number().optional(), 131 | cache_read_input_token_cost: z.number().optional(), 132 | // Context window limits from LiteLLM data 133 | max_tokens: z.number().optional(), 134 | max_input_tokens: z.number().optional(), 135 | max_output_tokens: z.number().optional(), 136 | }); 137 | 138 | /** 139 | * Type definition for model pricing information 140 | */ 141 | export type ModelPricing = z.infer; 142 | 143 | /** 144 | * Zod schema for Claude Code statusline hook JSON data 145 | */ 146 | export const statuslineHookJsonSchema = z.object({ 147 | session_id: z.string(), 148 | transcript_path: z.string(), 149 | cwd: z.string(), 150 | model: z.object({ 151 | id: z.string(), 152 | display_name: z.string(), 153 | }), 154 | workspace: z.object({ 155 | current_dir: z.string(), 156 | project_dir: z.string(), 157 | }), 158 | version: z.string().optional(), 159 | cost: z.object({ 160 | total_cost_usd: z.number(), 161 | total_duration_ms: z.number().optional(), 162 | total_api_duration_ms: z.number().optional(), 163 | total_lines_added: z.number().optional(), 164 | total_lines_removed: z.number().optional(), 165 | }).optional(), 166 | }); 167 | 168 | /** 169 | * Type definition for Claude Code statusline hook JSON data 170 | */ 171 | export type StatuslineHookJson = z.infer; 172 | 173 | /** 174 | * Type definition for transcript usage data from Claude messages 175 | */ 176 | 177 | export type TranscriptUsage = { 178 | input_tokens?: number; 179 | cache_creation_input_tokens?: number; 180 | cache_read_input_tokens?: number; 181 | output_tokens?: number; 182 | }; 183 | 184 | export type TranscriptMessage = { 185 | type?: string; 186 | message?: { 187 | usage?: TranscriptUsage; 188 | }; 189 | }; 190 | -------------------------------------------------------------------------------- /src/_utils.ts: -------------------------------------------------------------------------------- 1 | import { stat, utimes, writeFile } from 'node:fs/promises'; 2 | import { Result } from '@praha/byethrow'; 3 | import { createFixture } from 'fs-fixture'; 4 | 5 | export function unreachable(value: never): never { 6 | throw new Error(`Unreachable code reached with value: ${value as any}`); 7 | } 8 | 9 | /** 10 | * Gets the last modified time of a file using Result pattern 11 | * @param filePath - Path to the file 12 | * @returns Modification time in milliseconds, or 0 if file doesn't exist 13 | */ 14 | export async function getFileModifiedTime(filePath: string): Promise { 15 | return Result.pipe( 16 | Result.try({ 17 | try: stat(filePath), 18 | catch: error => error, 19 | }), 20 | Result.map(stats => stats.mtime.getTime()), 21 | Result.unwrap(0), // Default to 0 if file doesn't exist or can't be accessed 22 | ); 23 | } 24 | 25 | if (import.meta.vitest != null) { 26 | describe('unreachable', () => { 27 | it('should throw an error when called', () => { 28 | expect(() => unreachable('test' as never)).toThrow( 29 | 'Unreachable code reached with value: test', 30 | ); 31 | }); 32 | }); 33 | 34 | describe('getFileModifiedTime', () => { 35 | it('returns specific modification time when set', async () => { 36 | await using fixture = await createFixture({ 37 | 'test.txt': 'content', 38 | }); 39 | 40 | // Set specific time (2024-01-01 12:00:00 UTC) 41 | const specificTime = new Date('2024-01-01T12:00:00.000Z'); 42 | await utimes(`${fixture.path}/test.txt`, specificTime, specificTime); 43 | 44 | const mtime = await getFileModifiedTime(fixture.getPath('test.txt')); 45 | expect(mtime).toBe(specificTime.getTime()); 46 | expect(typeof mtime).toBe('number'); 47 | }); 48 | 49 | it('returns 0 for non-existent file', async () => { 50 | const mtime = await getFileModifiedTime('/non/existent/file.txt'); 51 | expect(mtime).toBe(0); 52 | }); 53 | 54 | it('detects file modification correctly', async () => { 55 | await using fixture = await createFixture({ 56 | 'test.txt': 'content', 57 | }); 58 | 59 | // Set first time 60 | const firstTime = new Date('2024-01-01T10:00:00.000Z'); 61 | await utimes(`${fixture.path}/test.txt`, firstTime, firstTime); 62 | 63 | const mtime1 = await getFileModifiedTime(`${fixture.path}/test.txt`); 64 | expect(mtime1).toBe(firstTime.getTime()); 65 | 66 | // Modify file and set second time 67 | const secondTime = new Date('2024-01-01T11:00:00.000Z'); 68 | await writeFile(fixture.getPath('test.txt'), 'modified content'); 69 | await utimes(fixture.getPath('test.txt'), secondTime, secondTime); 70 | 71 | const mtime2 = await getFileModifiedTime(fixture.getPath('test.txt')); 72 | expect(mtime2).toBe(secondTime.getTime()); 73 | expect(mtime2).toBeGreaterThan(mtime1); 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/calculate-cost.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Cost calculation utilities for usage data analysis 3 | * 4 | * This module provides functions for calculating costs and aggregating token usage 5 | * across different time periods and models. It handles both pre-calculated costs 6 | * and dynamic cost calculations based on model pricing. 7 | * 8 | * @module calculate-cost 9 | */ 10 | 11 | import type { AggregatedTokenCounts } from './_token-utils.ts'; 12 | import type { DailyUsage, MonthlyUsage, SessionUsage, WeeklyUsage } from './data-loader.ts'; 13 | import { getTotalTokens } from './_token-utils.ts'; 14 | import { 15 | createActivityDate, 16 | createDailyDate, 17 | createModelName, 18 | createProjectPath, 19 | createSessionId, 20 | createVersion, 21 | } from './_types.ts'; 22 | 23 | /** 24 | * Alias for AggregatedTokenCounts from shared utilities 25 | * @deprecated Use AggregatedTokenCounts from _token-utils.ts instead 26 | */ 27 | type TokenData = AggregatedTokenCounts; 28 | 29 | /** 30 | * Token totals including cost information 31 | */ 32 | type TokenTotals = TokenData & { 33 | totalCost: number; 34 | }; 35 | 36 | /** 37 | * Complete totals object with token counts, cost, and total token sum 38 | */ 39 | type TotalsObject = TokenTotals & { 40 | totalTokens: number; 41 | }; 42 | 43 | /** 44 | * Calculates total token usage and cost across multiple usage data entries 45 | * @param data - Array of daily, monthly, or session usage data 46 | * @returns Aggregated token totals and cost 47 | */ 48 | export function calculateTotals( 49 | data: Array, 50 | ): TokenTotals { 51 | return data.reduce( 52 | (acc, item) => ({ 53 | inputTokens: acc.inputTokens + item.inputTokens, 54 | outputTokens: acc.outputTokens + item.outputTokens, 55 | cacheCreationTokens: acc.cacheCreationTokens + item.cacheCreationTokens, 56 | cacheReadTokens: acc.cacheReadTokens + item.cacheReadTokens, 57 | totalCost: acc.totalCost + item.totalCost, 58 | }), 59 | { 60 | inputTokens: 0, 61 | outputTokens: 0, 62 | cacheCreationTokens: 0, 63 | cacheReadTokens: 0, 64 | totalCost: 0, 65 | }, 66 | ); 67 | } 68 | 69 | // Re-export getTotalTokens from shared utilities for backward compatibility 70 | export { getTotalTokens }; 71 | 72 | /** 73 | * Creates a complete totals object by adding total token count to existing totals 74 | * @param totals - Token totals with cost information 75 | * @returns Complete totals object including total token sum 76 | */ 77 | export function createTotalsObject(totals: TokenTotals): TotalsObject { 78 | return { 79 | ...totals, 80 | totalTokens: getTotalTokens(totals), 81 | }; 82 | } 83 | 84 | if (import.meta.vitest != null) { 85 | describe('token aggregation utilities', () => { 86 | it('calculateTotals should aggregate daily usage data', () => { 87 | const dailyData: DailyUsage[] = [ 88 | { 89 | date: createDailyDate('2024-01-01'), 90 | inputTokens: 100, 91 | outputTokens: 50, 92 | cacheCreationTokens: 25, 93 | cacheReadTokens: 10, 94 | totalCost: 0.01, 95 | modelsUsed: [createModelName('claude-sonnet-4-20250514')], 96 | modelBreakdowns: [], 97 | }, 98 | { 99 | date: createDailyDate('2024-01-02'), 100 | inputTokens: 200, 101 | outputTokens: 100, 102 | cacheCreationTokens: 50, 103 | cacheReadTokens: 20, 104 | totalCost: 0.02, 105 | modelsUsed: [createModelName('claude-opus-4-20250514')], 106 | modelBreakdowns: [], 107 | }, 108 | ]; 109 | 110 | const totals = calculateTotals(dailyData); 111 | expect(totals.inputTokens).toBe(300); 112 | expect(totals.outputTokens).toBe(150); 113 | expect(totals.cacheCreationTokens).toBe(75); 114 | expect(totals.cacheReadTokens).toBe(30); 115 | expect(totals.totalCost).toBeCloseTo(0.03); 116 | }); 117 | 118 | it('calculateTotals should aggregate session usage data', () => { 119 | const sessionData: SessionUsage[] = [ 120 | { 121 | sessionId: createSessionId('session-1'), 122 | projectPath: createProjectPath('project/path'), 123 | inputTokens: 100, 124 | outputTokens: 50, 125 | cacheCreationTokens: 25, 126 | cacheReadTokens: 10, 127 | totalCost: 0.01, 128 | lastActivity: createActivityDate('2024-01-01'), 129 | versions: [createVersion('1.0.3')], 130 | modelsUsed: [createModelName('claude-sonnet-4-20250514')], 131 | modelBreakdowns: [], 132 | }, 133 | { 134 | sessionId: createSessionId('session-2'), 135 | projectPath: createProjectPath('project/path'), 136 | inputTokens: 200, 137 | outputTokens: 100, 138 | cacheCreationTokens: 50, 139 | cacheReadTokens: 20, 140 | totalCost: 0.02, 141 | lastActivity: createActivityDate('2024-01-02'), 142 | versions: [createVersion('1.0.3'), createVersion('1.0.4')], 143 | modelsUsed: [createModelName('claude-opus-4-20250514')], 144 | modelBreakdowns: [], 145 | }, 146 | ]; 147 | 148 | const totals = calculateTotals(sessionData); 149 | expect(totals.inputTokens).toBe(300); 150 | expect(totals.outputTokens).toBe(150); 151 | expect(totals.cacheCreationTokens).toBe(75); 152 | expect(totals.cacheReadTokens).toBe(30); 153 | expect(totals.totalCost).toBeCloseTo(0.03); 154 | }); 155 | 156 | it('getTotalTokens should sum all token types', () => { 157 | const tokens = { 158 | inputTokens: 100, 159 | outputTokens: 50, 160 | cacheCreationTokens: 25, 161 | cacheReadTokens: 10, 162 | }; 163 | 164 | const total = getTotalTokens(tokens); 165 | expect(total).toBe(185); 166 | }); 167 | 168 | it('getTotalTokens should handle zero values', () => { 169 | const tokens = { 170 | inputTokens: 0, 171 | outputTokens: 0, 172 | cacheCreationTokens: 0, 173 | cacheReadTokens: 0, 174 | }; 175 | 176 | const total = getTotalTokens(tokens); 177 | expect(total).toBe(0); 178 | }); 179 | 180 | it('createTotalsObject should create complete totals object', () => { 181 | const totals = { 182 | inputTokens: 100, 183 | outputTokens: 50, 184 | cacheCreationTokens: 25, 185 | cacheReadTokens: 10, 186 | totalCost: 0.01, 187 | }; 188 | 189 | const totalsObject = createTotalsObject(totals); 190 | expect(totalsObject).toEqual({ 191 | inputTokens: 100, 192 | outputTokens: 50, 193 | cacheCreationTokens: 25, 194 | cacheReadTokens: 10, 195 | totalTokens: 185, 196 | totalCost: 0.01, 197 | }); 198 | }); 199 | 200 | it('calculateTotals should handle empty array', () => { 201 | const totals = calculateTotals([]); 202 | expect(totals).toEqual({ 203 | inputTokens: 0, 204 | outputTokens: 0, 205 | cacheCreationTokens: 0, 206 | cacheReadTokens: 0, 207 | totalCost: 0, 208 | }); 209 | }); 210 | }); 211 | } 212 | -------------------------------------------------------------------------------- /src/commands/_blocks.live.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Live monitoring command orchestration 3 | * 4 | * This module provides the command-line interface for live monitoring, 5 | * handling process lifecycle, signal management, and terminal setup. 6 | * The actual rendering logic is handled by the _live-rendering module. 7 | */ 8 | 9 | import type { LiveMonitoringConfig } from '../_live-rendering.ts'; 10 | import process from 'node:process'; 11 | import { Result } from '@praha/byethrow'; 12 | import pc from 'picocolors'; 13 | import { MIN_RENDER_INTERVAL_MS } from '../_consts.ts'; 14 | import { 15 | clearLiveMonitorCache, 16 | createLiveMonitorState, 17 | getActiveBlock, 18 | } from '../_live-monitor.ts'; 19 | import { 20 | delayWithAbort, 21 | renderActiveBlock, 22 | renderWaitingState, 23 | } from '../_live-rendering.ts'; 24 | import { TerminalManager } from '../_terminal-utils.ts'; 25 | import { logger } from '../logger.ts'; 26 | 27 | export async function startLiveMonitoring(config: LiveMonitoringConfig): Promise { 28 | const terminal = new TerminalManager(); 29 | const abortController = new AbortController(); 30 | let lastRenderTime = 0; 31 | 32 | // Create live monitor state with efficient data loading 33 | const monitorConfig = { 34 | claudePaths: config.claudePaths, 35 | sessionDurationHours: config.sessionDurationHours, 36 | mode: config.mode, 37 | order: config.order, 38 | }; 39 | using monitorState = createLiveMonitorState(monitorConfig); 40 | 41 | // Setup graceful shutdown 42 | const cleanup = (): void => { 43 | abortController.abort(); 44 | terminal.cleanup(); 45 | terminal.clearScreen(); 46 | logger.info('Live monitoring stopped.'); 47 | if (process.exitCode == null) { 48 | process.exit(0); 49 | } 50 | }; 51 | 52 | process.on('SIGINT', cleanup); 53 | process.on('SIGTERM', cleanup); 54 | 55 | // Setup terminal for optimal TUI performance 56 | terminal.enterAlternateScreen(); 57 | terminal.enableSyncMode(); 58 | terminal.clearScreen(); 59 | terminal.hideCursor(); 60 | 61 | const monitoringResult = await Result.try({ 62 | try: async () => { 63 | while (!abortController.signal.aborted) { 64 | const now = Date.now(); 65 | const timeSinceLastRender = now - lastRenderTime; 66 | 67 | // Skip render if too soon (frame rate limiting) 68 | if (timeSinceLastRender < MIN_RENDER_INTERVAL_MS) { 69 | await delayWithAbort(MIN_RENDER_INTERVAL_MS - timeSinceLastRender, abortController.signal); 70 | continue; 71 | } 72 | 73 | // Get latest data 74 | const activeBlock = await getActiveBlock(monitorState, monitorConfig); 75 | clearLiveMonitorCache(monitorState); // Clear cache for memory management 76 | 77 | if (activeBlock == null) { 78 | await renderWaitingState(terminal, config, abortController.signal); 79 | continue; 80 | } 81 | 82 | // Render active block 83 | renderActiveBlock(terminal, activeBlock, config); 84 | lastRenderTime = Date.now(); 85 | 86 | // Wait before next refresh (refreshInterval passed, aborted, or terminal resized) 87 | let resizeEventHandler: undefined | ((value: unknown) => void); 88 | 89 | try { 90 | await Promise.race([ 91 | delayWithAbort(config.refreshInterval, abortController.signal), 92 | new Promise((resolve) => { 93 | resizeEventHandler = resolve; 94 | process.stdout.once('resize', resolve); 95 | }), 96 | ]); 97 | } 98 | finally { 99 | if (resizeEventHandler != null) { 100 | process.stdout.removeListener('resize', resizeEventHandler); 101 | } 102 | } 103 | } 104 | }, 105 | catch: error => error, 106 | })(); 107 | 108 | if (Result.isFailure(monitoringResult)) { 109 | const error = monitoringResult.error; 110 | if ((error instanceof DOMException || error instanceof Error) && error.name === 'AbortError') { 111 | return; // Normal graceful shutdown 112 | } 113 | 114 | // Handle and display errors 115 | const errorMessage = error instanceof Error ? error.message : String(error); 116 | terminal.startBuffering(); 117 | terminal.clearScreen(); 118 | terminal.write(pc.red(`Error: ${errorMessage}\n`)); 119 | terminal.flush(); 120 | logger.error(`Live monitoring error: ${errorMessage}`); 121 | await delayWithAbort(config.refreshInterval, abortController.signal).catch(() => {}); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/commands/_session_id.ts: -------------------------------------------------------------------------------- 1 | import type { CostMode } from '../_types.ts'; 2 | import type { UsageData } from '../data-loader.ts'; 3 | import process from 'node:process'; 4 | import { Result } from '@praha/byethrow'; 5 | import { formatDateCompact } from '../_date-utils.ts'; 6 | import { processWithJq } from '../_jq-processor.ts'; 7 | import { formatCurrency, formatNumber, ResponsiveTable } from '../_table.ts'; 8 | import { loadSessionUsageById } from '../data-loader.ts'; 9 | import { log, logger } from '../logger.ts'; 10 | 11 | export type SessionIdContext = { 12 | values: { 13 | id: string; 14 | mode: CostMode; 15 | offline: boolean; 16 | jq?: string; 17 | timezone?: string; 18 | locale: string; // normalized to non-optional to avoid touching data-loader 19 | }; 20 | }; 21 | 22 | /** 23 | * Handles the session ID lookup and displays usage data. 24 | */ 25 | export async function handleSessionIdLookup(ctx: SessionIdContext, useJson: boolean): Promise { 26 | const sessionUsage = await loadSessionUsageById(ctx.values.id, { 27 | mode: ctx.values.mode, 28 | offline: ctx.values.offline, 29 | }); 30 | 31 | if (sessionUsage == null) { 32 | if (useJson) { 33 | log(JSON.stringify(null)); 34 | } 35 | else { 36 | logger.warn(`No session found with ID: ${ctx.values.id}`); 37 | } 38 | process.exit(0); 39 | } 40 | 41 | if (useJson) { 42 | const jsonOutput = { 43 | sessionId: ctx.values.id, 44 | totalCost: sessionUsage.totalCost, 45 | totalTokens: calculateSessionTotalTokens(sessionUsage.entries), 46 | entries: sessionUsage.entries.map(entry => ({ 47 | timestamp: entry.timestamp, 48 | inputTokens: entry.message.usage.input_tokens, 49 | outputTokens: entry.message.usage.output_tokens, 50 | cacheCreationTokens: entry.message.usage.cache_creation_input_tokens ?? 0, 51 | cacheReadTokens: entry.message.usage.cache_read_input_tokens ?? 0, 52 | model: entry.message.model ?? 'unknown', 53 | costUSD: entry.costUSD ?? 0, 54 | })), 55 | }; 56 | 57 | if (ctx.values.jq != null) { 58 | const jqResult = await processWithJq(jsonOutput, ctx.values.jq); 59 | if (Result.isFailure(jqResult)) { 60 | logger.error(jqResult.error.message); 61 | process.exit(1); 62 | } 63 | log(jqResult.value); 64 | } 65 | else { 66 | log(JSON.stringify(jsonOutput, null, 2)); 67 | } 68 | } 69 | else { 70 | logger.box(`Claude Code Session Usage - ${ctx.values.id}`); 71 | 72 | const totalTokens = calculateSessionTotalTokens(sessionUsage.entries); 73 | 74 | log(`Total Cost: ${formatCurrency(sessionUsage.totalCost)}`); 75 | log(`Total Tokens: ${formatNumber(totalTokens)}`); 76 | log(`Total Entries: ${sessionUsage.entries.length}`); 77 | log(''); 78 | 79 | if (sessionUsage.entries.length > 0) { 80 | const table = new ResponsiveTable({ 81 | head: [ 82 | 'Timestamp', 83 | 'Model', 84 | 'Input', 85 | 'Output', 86 | 'Cache Create', 87 | 'Cache Read', 88 | 'Cost (USD)', 89 | ], 90 | style: { head: ['cyan'] }, 91 | colAligns: ['left', 'left', 'right', 'right', 'right', 'right', 'right'], 92 | }); 93 | 94 | for (const entry of sessionUsage.entries) { 95 | table.push([ 96 | formatDateCompact(entry.timestamp, ctx.values.timezone, ctx.values.locale), 97 | entry.message.model ?? 'unknown', 98 | formatNumber(entry.message.usage.input_tokens), 99 | formatNumber(entry.message.usage.output_tokens), 100 | formatNumber(entry.message.usage.cache_creation_input_tokens ?? 0), 101 | formatNumber(entry.message.usage.cache_read_input_tokens ?? 0), 102 | formatCurrency(entry.costUSD ?? 0), 103 | ]); 104 | } 105 | 106 | log(table.toString()); 107 | } 108 | } 109 | } 110 | 111 | function calculateSessionTotalTokens(entries: UsageData[]): number { 112 | return entries.reduce((sum, entry) => { 113 | const usage = entry.message.usage; 114 | return ( 115 | sum 116 | + usage.input_tokens 117 | + usage.output_tokens 118 | + (usage.cache_creation_input_tokens ?? 0) 119 | + (usage.cache_read_input_tokens ?? 0) 120 | ); 121 | }, 0); 122 | } 123 | -------------------------------------------------------------------------------- /src/commands/daily.ts: -------------------------------------------------------------------------------- 1 | import type { UsageReportConfig } from '../_table.ts'; 2 | import process from 'node:process'; 3 | import { Result } from '@praha/byethrow'; 4 | import { define } from 'gunshi'; 5 | import pc from 'picocolors'; 6 | import { loadConfig, mergeConfigWithArgs } from '../_config-loader-tokens.ts'; 7 | import { groupByProject, groupDataByProject } from '../_daily-grouping.ts'; 8 | import { formatDateCompact } from '../_date-utils.ts'; 9 | import { processWithJq } from '../_jq-processor.ts'; 10 | import { formatProjectName } from '../_project-names.ts'; 11 | import { sharedCommandConfig } from '../_shared-args.ts'; 12 | import { addEmptySeparatorRow, createUsageReportTable, formatTotalsRow, formatUsageDataRow, pushBreakdownRows } from '../_table.ts'; 13 | import { 14 | calculateTotals, 15 | createTotalsObject, 16 | getTotalTokens, 17 | } from '../calculate-cost.ts'; 18 | import { loadDailyUsageData } from '../data-loader.ts'; 19 | import { detectMismatches, printMismatchReport } from '../debug.ts'; 20 | import { log, logger } from '../logger.ts'; 21 | 22 | export const dailyCommand = define({ 23 | name: 'daily', 24 | description: 'Show usage report grouped by date', 25 | ...sharedCommandConfig, 26 | args: { 27 | ...sharedCommandConfig.args, 28 | instances: { 29 | type: 'boolean', 30 | short: 'i', 31 | description: 'Show usage breakdown by project/instance', 32 | default: false, 33 | }, 34 | project: { 35 | type: 'string', 36 | short: 'p', 37 | description: 'Filter to specific project name', 38 | }, 39 | projectAliases: { 40 | type: 'string', 41 | description: 'Comma-separated project aliases (e.g., \'ccusage=Usage Tracker,myproject=My Project\')', 42 | hidden: true, 43 | }, 44 | }, 45 | async run(ctx) { 46 | // Load configuration and merge with CLI arguments 47 | const config = loadConfig(ctx.values.config, ctx.values.debug); 48 | const mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug); 49 | 50 | // Convert projectAliases to Map if it exists 51 | // Parse comma-separated key=value pairs 52 | let projectAliases: Map | undefined; 53 | if (mergedOptions.projectAliases != null && typeof mergedOptions.projectAliases === 'string') { 54 | projectAliases = new Map(); 55 | const pairs = mergedOptions.projectAliases.split(',').map(pair => pair.trim()).filter(pair => pair !== ''); 56 | for (const pair of pairs) { 57 | const parts = pair.split('=').map(s => s.trim()); 58 | const rawName = parts[0]; 59 | const alias = parts[1]; 60 | if (rawName != null && alias != null && rawName !== '' && alias !== '') { 61 | projectAliases.set(rawName, alias); 62 | } 63 | } 64 | } 65 | 66 | // --jq implies --json 67 | const useJson = Boolean(mergedOptions.json) || mergedOptions.jq != null; 68 | if (useJson) { 69 | logger.level = 0; 70 | } 71 | 72 | const dailyData = await loadDailyUsageData({ 73 | ...mergedOptions, 74 | groupByProject: mergedOptions.instances, 75 | }); 76 | 77 | if (dailyData.length === 0) { 78 | if (useJson) { 79 | log(JSON.stringify([])); 80 | } 81 | else { 82 | logger.warn('No Claude usage data found.'); 83 | } 84 | process.exit(0); 85 | } 86 | 87 | // Calculate totals 88 | const totals = calculateTotals(dailyData); 89 | 90 | // Show debug information if requested 91 | if (mergedOptions.debug && !useJson) { 92 | const mismatchStats = await detectMismatches(undefined); 93 | printMismatchReport(mismatchStats, mergedOptions.debugSamples as number | undefined); 94 | } 95 | 96 | if (useJson) { 97 | // Output JSON format - group by project if instances flag is used 98 | const jsonOutput = Boolean(mergedOptions.instances) && dailyData.some(d => d.project != null) 99 | ? { 100 | projects: groupByProject(dailyData), 101 | totals: createTotalsObject(totals), 102 | } 103 | : { 104 | daily: dailyData.map(data => ({ 105 | date: data.date, 106 | inputTokens: data.inputTokens, 107 | outputTokens: data.outputTokens, 108 | cacheCreationTokens: data.cacheCreationTokens, 109 | cacheReadTokens: data.cacheReadTokens, 110 | totalTokens: getTotalTokens(data), 111 | totalCost: data.totalCost, 112 | modelsUsed: data.modelsUsed, 113 | modelBreakdowns: data.modelBreakdowns, 114 | ...(data.project != null && { project: data.project }), 115 | })), 116 | totals: createTotalsObject(totals), 117 | }; 118 | 119 | // Process with jq if specified 120 | if (mergedOptions.jq != null) { 121 | const jqResult = await processWithJq(jsonOutput, mergedOptions.jq); 122 | if (Result.isFailure(jqResult)) { 123 | logger.error((jqResult.error).message); 124 | process.exit(1); 125 | } 126 | log(jqResult.value); 127 | } 128 | else { 129 | log(JSON.stringify(jsonOutput, null, 2)); 130 | } 131 | } 132 | else { 133 | // Print header 134 | logger.box('Claude Code Token Usage Report - Daily'); 135 | 136 | // Create table with compact mode support 137 | const tableConfig: UsageReportConfig = { 138 | firstColumnName: 'Date', 139 | dateFormatter: (dateStr: string) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? undefined), 140 | forceCompact: ctx.values.compact, 141 | }; 142 | const table = createUsageReportTable(tableConfig); 143 | 144 | // Add daily data - group by project if instances flag is used 145 | if (Boolean(mergedOptions.instances) && dailyData.some(d => d.project != null)) { 146 | // Group data by project for visual separation 147 | const projectGroups = groupDataByProject(dailyData); 148 | 149 | let isFirstProject = true; 150 | for (const [projectName, projectData] of Object.entries(projectGroups)) { 151 | // Add project section header 152 | if (!isFirstProject) { 153 | // Add empty row for visual separation between projects 154 | table.push(['', '', '', '', '', '', '', '']); 155 | } 156 | 157 | // Add project header row 158 | table.push([ 159 | pc.cyan(`Project: ${formatProjectName(projectName, projectAliases)}`), 160 | '', 161 | '', 162 | '', 163 | '', 164 | '', 165 | '', 166 | '', 167 | ]); 168 | 169 | // Add data rows for this project 170 | for (const data of projectData) { 171 | const row = formatUsageDataRow(data.date, { 172 | inputTokens: data.inputTokens, 173 | outputTokens: data.outputTokens, 174 | cacheCreationTokens: data.cacheCreationTokens, 175 | cacheReadTokens: data.cacheReadTokens, 176 | totalCost: data.totalCost, 177 | modelsUsed: data.modelsUsed, 178 | }); 179 | table.push(row); 180 | 181 | // Add model breakdown rows if flag is set 182 | if (mergedOptions.breakdown) { 183 | pushBreakdownRows(table, data.modelBreakdowns); 184 | } 185 | } 186 | 187 | isFirstProject = false; 188 | } 189 | } 190 | else { 191 | // Standard display without project grouping 192 | for (const data of dailyData) { 193 | // Main row 194 | const row = formatUsageDataRow(data.date, { 195 | inputTokens: data.inputTokens, 196 | outputTokens: data.outputTokens, 197 | cacheCreationTokens: data.cacheCreationTokens, 198 | cacheReadTokens: data.cacheReadTokens, 199 | totalCost: data.totalCost, 200 | modelsUsed: data.modelsUsed, 201 | }); 202 | table.push(row); 203 | 204 | // Add model breakdown rows if flag is set 205 | if (mergedOptions.breakdown) { 206 | pushBreakdownRows(table, data.modelBreakdowns); 207 | } 208 | } 209 | } 210 | 211 | // Add empty row for visual separation before totals 212 | addEmptySeparatorRow(table, 8); 213 | 214 | // Add totals 215 | const totalsRow = formatTotalsRow({ 216 | inputTokens: totals.inputTokens, 217 | outputTokens: totals.outputTokens, 218 | cacheCreationTokens: totals.cacheCreationTokens, 219 | cacheReadTokens: totals.cacheReadTokens, 220 | totalCost: totals.totalCost, 221 | }); 222 | table.push(totalsRow); 223 | 224 | log(table.toString()); 225 | 226 | // Show guidance message if in compact mode 227 | if (table.isCompactMode()) { 228 | logger.info('\nRunning in Compact Mode'); 229 | logger.info('Expand terminal width to see cache metrics and total tokens'); 230 | } 231 | } 232 | }, 233 | }); 234 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { cli } from 'gunshi'; 3 | import { description, name, version } from '../../package.json'; 4 | import { blocksCommand } from './blocks.ts'; 5 | import { dailyCommand } from './daily.ts'; 6 | import { mcpCommand } from './mcp.ts'; 7 | import { monthlyCommand } from './monthly.ts'; 8 | import { sessionCommand } from './session.ts'; 9 | import { statuslineCommand } from './statusline.ts'; 10 | import { weeklyCommand } from './weekly.ts'; 11 | 12 | // Re-export all commands for easy importing 13 | export { blocksCommand, dailyCommand, mcpCommand, monthlyCommand, sessionCommand, statuslineCommand, weeklyCommand }; 14 | 15 | /** 16 | * Command entries as tuple array 17 | */ 18 | export const subCommandUnion = [ 19 | ['daily', dailyCommand], 20 | ['monthly', monthlyCommand], 21 | ['weekly', weeklyCommand], 22 | ['session', sessionCommand], 23 | ['blocks', blocksCommand], 24 | ['mcp', mcpCommand], 25 | ['statusline', statuslineCommand], 26 | ] as const; 27 | 28 | /** 29 | * Available command names extracted from union 30 | */ 31 | export type CommandName = typeof subCommandUnion[number][0]; 32 | 33 | /** 34 | * Map of available CLI subcommands 35 | */ 36 | const subCommands = new Map(); 37 | for (const [name, command] of subCommandUnion) { 38 | subCommands.set(name, command); 39 | } 40 | 41 | /** 42 | * Default command when no subcommand is specified (defaults to daily) 43 | */ 44 | const mainCommand = dailyCommand; 45 | 46 | export async function run(): Promise { 47 | await cli(process.argv.slice(2), mainCommand, { 48 | name, 49 | version, 50 | description, 51 | subCommands, 52 | renderHeader: null, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/commands/mcp.ts: -------------------------------------------------------------------------------- 1 | import { serve } from '@hono/node-server'; 2 | import { define } from 'gunshi'; 3 | import { MCP_DEFAULT_PORT } from '../_consts.ts'; 4 | import { sharedArgs } from '../_shared-args.ts'; 5 | import { getClaudePaths } from '../data-loader.ts'; 6 | import { logger } from '../logger.ts'; 7 | import { createMcpHttpApp, createMcpServer, startMcpServerStdio } from '../mcp.ts'; 8 | 9 | /** 10 | * MCP server command that supports both stdio and HTTP transports. 11 | * Allows starting an MCP server for external integrations with usage reporting tools. 12 | */ 13 | export const mcpCommand = define({ 14 | name: 'mcp', 15 | description: 'Start MCP server with usage reporting tools', 16 | args: { 17 | mode: sharedArgs.mode, 18 | type: { 19 | type: 'enum', 20 | short: 't', 21 | description: 'Transport type for MCP server', 22 | choices: ['stdio', 'http'] as const, 23 | default: 'stdio', 24 | }, 25 | port: { 26 | type: 'number', 27 | description: `Port for HTTP transport (default: ${MCP_DEFAULT_PORT})`, 28 | default: MCP_DEFAULT_PORT, 29 | }, 30 | }, 31 | async run(ctx) { 32 | const { type, mode, port } = ctx.values; 33 | // disable info logging for stdio 34 | if (type === 'stdio') { 35 | logger.level = 0; 36 | } 37 | 38 | const paths = getClaudePaths(); 39 | if (paths.length === 0) { 40 | logger.error('No valid Claude data directory found'); 41 | throw new Error('No valid Claude data directory found'); 42 | } 43 | 44 | const options = { 45 | claudePath: paths[0]!, 46 | mode, 47 | }; 48 | 49 | if (type === 'stdio') { 50 | const server = createMcpServer(options); 51 | await startMcpServerStdio(server); 52 | } 53 | else { 54 | const app = createMcpHttpApp(options); 55 | // Use the Hono app to handle requests 56 | serve({ 57 | fetch: app.fetch, 58 | port, 59 | }); 60 | logger.info(`MCP server is running on http://localhost:${port}`); 61 | } 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /src/commands/monthly.ts: -------------------------------------------------------------------------------- 1 | import type { UsageReportConfig } from '../_table.ts'; 2 | import process from 'node:process'; 3 | import { Result } from '@praha/byethrow'; 4 | import { define } from 'gunshi'; 5 | import { loadConfig, mergeConfigWithArgs } from '../_config-loader-tokens.ts'; 6 | import { DEFAULT_LOCALE } from '../_consts.ts'; 7 | import { formatDateCompact } from '../_date-utils.ts'; 8 | import { processWithJq } from '../_jq-processor.ts'; 9 | import { sharedCommandConfig } from '../_shared-args.ts'; 10 | import { addEmptySeparatorRow, createUsageReportTable, formatTotalsRow, formatUsageDataRow, pushBreakdownRows } from '../_table.ts'; 11 | import { 12 | calculateTotals, 13 | createTotalsObject, 14 | getTotalTokens, 15 | } from '../calculate-cost.ts'; 16 | import { loadMonthlyUsageData } from '../data-loader.ts'; 17 | import { detectMismatches, printMismatchReport } from '../debug.ts'; 18 | import { log, logger } from '../logger.ts'; 19 | 20 | export const monthlyCommand = define({ 21 | name: 'monthly', 22 | description: 'Show usage report grouped by month', 23 | ...sharedCommandConfig, 24 | async run(ctx) { 25 | // Load configuration and merge with CLI arguments 26 | const config = loadConfig(ctx.values.config, ctx.values.debug); 27 | const mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug); 28 | 29 | // --jq implies --json 30 | const useJson = Boolean(mergedOptions.json) || mergedOptions.jq != null; 31 | if (useJson) { 32 | logger.level = 0; 33 | } 34 | 35 | const monthlyData = await loadMonthlyUsageData(mergedOptions); 36 | 37 | if (monthlyData.length === 0) { 38 | if (useJson) { 39 | const emptyOutput = { 40 | monthly: [], 41 | totals: { 42 | inputTokens: 0, 43 | outputTokens: 0, 44 | cacheCreationTokens: 0, 45 | cacheReadTokens: 0, 46 | totalTokens: 0, 47 | totalCost: 0, 48 | }, 49 | }; 50 | log(JSON.stringify(emptyOutput, null, 2)); 51 | } 52 | else { 53 | logger.warn('No Claude usage data found.'); 54 | } 55 | process.exit(0); 56 | } 57 | 58 | // Calculate totals 59 | const totals = calculateTotals(monthlyData); 60 | 61 | // Show debug information if requested 62 | if (mergedOptions.debug && !useJson) { 63 | const mismatchStats = await detectMismatches(undefined); 64 | printMismatchReport(mismatchStats, mergedOptions.debugSamples as number | undefined); 65 | } 66 | 67 | if (useJson) { 68 | // Output JSON format 69 | const jsonOutput = { 70 | monthly: monthlyData.map(data => ({ 71 | month: data.month, 72 | inputTokens: data.inputTokens, 73 | outputTokens: data.outputTokens, 74 | cacheCreationTokens: data.cacheCreationTokens, 75 | cacheReadTokens: data.cacheReadTokens, 76 | totalTokens: getTotalTokens(data), 77 | totalCost: data.totalCost, 78 | modelsUsed: data.modelsUsed, 79 | modelBreakdowns: data.modelBreakdowns, 80 | })), 81 | totals: createTotalsObject(totals), 82 | }; 83 | 84 | // Process with jq if specified 85 | if (mergedOptions.jq != null) { 86 | const jqResult = await processWithJq(jsonOutput, mergedOptions.jq); 87 | if (Result.isFailure(jqResult)) { 88 | logger.error((jqResult.error).message); 89 | process.exit(1); 90 | } 91 | log(jqResult.value); 92 | } 93 | else { 94 | log(JSON.stringify(jsonOutput, null, 2)); 95 | } 96 | } 97 | else { 98 | // Print header 99 | logger.box('Claude Code Token Usage Report - Monthly'); 100 | 101 | // Create table with compact mode support 102 | const tableConfig: UsageReportConfig = { 103 | firstColumnName: 'Month', 104 | dateFormatter: (dateStr: string) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? DEFAULT_LOCALE), 105 | forceCompact: ctx.values.compact, 106 | }; 107 | const table = createUsageReportTable(tableConfig); 108 | 109 | // Add monthly data 110 | for (const data of monthlyData) { 111 | // Main row 112 | const row = formatUsageDataRow(data.month, { 113 | inputTokens: data.inputTokens, 114 | outputTokens: data.outputTokens, 115 | cacheCreationTokens: data.cacheCreationTokens, 116 | cacheReadTokens: data.cacheReadTokens, 117 | totalCost: data.totalCost, 118 | modelsUsed: data.modelsUsed, 119 | }); 120 | table.push(row); 121 | 122 | // Add model breakdown rows if flag is set 123 | if (mergedOptions.breakdown) { 124 | pushBreakdownRows(table, data.modelBreakdowns); 125 | } 126 | } 127 | 128 | // Add empty row for visual separation before totals 129 | addEmptySeparatorRow(table, 8); 130 | 131 | // Add totals 132 | const totalsRow = formatTotalsRow({ 133 | inputTokens: totals.inputTokens, 134 | outputTokens: totals.outputTokens, 135 | cacheCreationTokens: totals.cacheCreationTokens, 136 | cacheReadTokens: totals.cacheReadTokens, 137 | totalCost: totals.totalCost, 138 | }); 139 | table.push(totalsRow); 140 | 141 | log(table.toString()); 142 | 143 | // Show guidance message if in compact mode 144 | if (table.isCompactMode()) { 145 | logger.info('\nRunning in Compact Mode'); 146 | logger.info('Expand terminal width to see cache metrics and total tokens'); 147 | } 148 | } 149 | }, 150 | }); 151 | -------------------------------------------------------------------------------- /src/commands/session.ts: -------------------------------------------------------------------------------- 1 | import type { UsageReportConfig } from '../_table.ts'; 2 | // Types not needed here after extracting --id logic 3 | import process from 'node:process'; 4 | import { Result } from '@praha/byethrow'; 5 | import { define } from 'gunshi'; 6 | import { loadConfig, mergeConfigWithArgs } from '../_config-loader-tokens.ts'; 7 | import { DEFAULT_LOCALE } from '../_consts.ts'; 8 | import { formatDateCompact } from '../_date-utils.ts'; 9 | import { processWithJq } from '../_jq-processor.ts'; 10 | import { sharedCommandConfig } from '../_shared-args.ts'; 11 | import { addEmptySeparatorRow, createUsageReportTable, formatTotalsRow, formatUsageDataRow, pushBreakdownRows } from '../_table.ts'; 12 | import { 13 | calculateTotals, 14 | createTotalsObject, 15 | getTotalTokens, 16 | } from '../calculate-cost.ts'; 17 | import { loadSessionData } from '../data-loader.ts'; 18 | import { detectMismatches, printMismatchReport } from '../debug.ts'; 19 | import { log, logger } from '../logger.ts'; 20 | import { handleSessionIdLookup } from './_session_id.ts'; 21 | 22 | // All --id logic moved to ./_session_id.ts 23 | 24 | export const sessionCommand = define({ 25 | name: 'session', 26 | description: 'Show usage report grouped by conversation session', 27 | ...sharedCommandConfig, 28 | args: { 29 | ...sharedCommandConfig.args, 30 | id: { 31 | type: 'string', 32 | short: 'i', 33 | description: 'Load usage data for a specific session ID', 34 | }, 35 | }, 36 | toKebab: true, 37 | async run(ctx): Promise { 38 | // Load configuration and merge with CLI arguments 39 | const config = loadConfig(ctx.values.config, ctx.values.debug); 40 | const mergedOptions: typeof ctx.values = mergeConfigWithArgs(ctx, config, ctx.values.debug); 41 | 42 | // --jq implies --json 43 | const useJson = mergedOptions.json || mergedOptions.jq != null; 44 | if (useJson) { 45 | logger.level = 0; 46 | } 47 | 48 | // Handle specific session ID lookup 49 | if (mergedOptions.id != null) { 50 | return handleSessionIdLookup({ 51 | values: { 52 | id: mergedOptions.id, 53 | mode: mergedOptions.mode, 54 | offline: mergedOptions.offline, 55 | jq: mergedOptions.jq, 56 | timezone: mergedOptions.timezone, 57 | locale: mergedOptions.locale ?? DEFAULT_LOCALE, 58 | }, 59 | }, useJson); 60 | } 61 | 62 | // Original session listing logic 63 | const sessionData = await loadSessionData({ 64 | since: ctx.values.since, 65 | until: ctx.values.until, 66 | mode: ctx.values.mode, 67 | order: ctx.values.order, 68 | offline: ctx.values.offline, 69 | timezone: ctx.values.timezone, 70 | locale: ctx.values.locale, 71 | }); 72 | 73 | if (sessionData.length === 0) { 74 | if (useJson) { 75 | log(JSON.stringify([])); 76 | } 77 | else { 78 | logger.warn('No Claude usage data found.'); 79 | } 80 | process.exit(0); 81 | } 82 | 83 | // Calculate totals 84 | const totals = calculateTotals(sessionData); 85 | 86 | // Show debug information if requested 87 | if (ctx.values.debug && !useJson) { 88 | const mismatchStats = await detectMismatches(undefined); 89 | printMismatchReport(mismatchStats, ctx.values.debugSamples); 90 | } 91 | 92 | if (useJson) { 93 | // Output JSON format 94 | const jsonOutput = { 95 | sessions: sessionData.map(data => ({ 96 | sessionId: data.sessionId, 97 | inputTokens: data.inputTokens, 98 | outputTokens: data.outputTokens, 99 | cacheCreationTokens: data.cacheCreationTokens, 100 | cacheReadTokens: data.cacheReadTokens, 101 | totalTokens: getTotalTokens(data), 102 | totalCost: data.totalCost, 103 | lastActivity: data.lastActivity, 104 | modelsUsed: data.modelsUsed, 105 | modelBreakdowns: data.modelBreakdowns, 106 | projectPath: data.projectPath, 107 | })), 108 | totals: createTotalsObject(totals), 109 | }; 110 | 111 | // Process with jq if specified 112 | if (ctx.values.jq != null) { 113 | const jqResult = await processWithJq(jsonOutput, ctx.values.jq); 114 | if (Result.isFailure(jqResult)) { 115 | logger.error((jqResult.error).message); 116 | process.exit(1); 117 | } 118 | log(jqResult.value); 119 | } 120 | else { 121 | log(JSON.stringify(jsonOutput, null, 2)); 122 | } 123 | } 124 | else { 125 | // Print header 126 | logger.box('Claude Code Token Usage Report - By Session'); 127 | 128 | // Create table with compact mode support 129 | const tableConfig: UsageReportConfig = { 130 | firstColumnName: 'Session', 131 | includeLastActivity: true, 132 | dateFormatter: (dateStr: string) => formatDateCompact(dateStr, ctx.values.timezone, ctx.values.locale), 133 | forceCompact: ctx.values.compact, 134 | }; 135 | const table = createUsageReportTable(tableConfig); 136 | 137 | // Add session data 138 | let maxSessionLength = 0; 139 | for (const data of sessionData) { 140 | const sessionDisplay = data.sessionId.split('-').slice(-2).join('-'); // Display last two parts of session ID 141 | 142 | maxSessionLength = Math.max(maxSessionLength, sessionDisplay.length); 143 | 144 | // Main row 145 | const row = formatUsageDataRow(sessionDisplay, { 146 | inputTokens: data.inputTokens, 147 | outputTokens: data.outputTokens, 148 | cacheCreationTokens: data.cacheCreationTokens, 149 | cacheReadTokens: data.cacheReadTokens, 150 | totalCost: data.totalCost, 151 | modelsUsed: data.modelsUsed, 152 | }, data.lastActivity); 153 | table.push(row); 154 | 155 | // Add model breakdown rows if flag is set 156 | if (ctx.values.breakdown) { 157 | // Session has 1 extra column before data and 1 trailing column 158 | pushBreakdownRows(table, data.modelBreakdowns, 1, 1); 159 | } 160 | } 161 | 162 | // Add empty row for visual separation before totals 163 | addEmptySeparatorRow(table, 9); 164 | 165 | // Add totals 166 | const totalsRow = formatTotalsRow({ 167 | inputTokens: totals.inputTokens, 168 | outputTokens: totals.outputTokens, 169 | cacheCreationTokens: totals.cacheCreationTokens, 170 | cacheReadTokens: totals.cacheReadTokens, 171 | totalCost: totals.totalCost, 172 | }, true); // Include Last Activity column 173 | table.push(totalsRow); 174 | 175 | log(table.toString()); 176 | 177 | // Show guidance message if in compact mode 178 | if (table.isCompactMode()) { 179 | logger.info('\nRunning in Compact Mode'); 180 | logger.info('Expand terminal width to see cache metrics and total tokens'); 181 | } 182 | } 183 | }, 184 | }); 185 | 186 | // Note: Tests for --id functionality are covered by the existing loadSessionUsageById tests 187 | // in data-loader.ts, since this command directly uses that function. 188 | -------------------------------------------------------------------------------- /src/commands/weekly.ts: -------------------------------------------------------------------------------- 1 | import type { UsageReportConfig } from '../_table.ts'; 2 | import process from 'node:process'; 3 | import { Result } from '@praha/byethrow'; 4 | import { define } from 'gunshi'; 5 | import { loadConfig, mergeConfigWithArgs } from '../_config-loader-tokens.ts'; 6 | import { WEEK_DAYS } from '../_consts.ts'; 7 | import { formatDateCompact } from '../_date-utils.ts'; 8 | import { processWithJq } from '../_jq-processor.ts'; 9 | import { sharedArgs } from '../_shared-args.ts'; 10 | import { addEmptySeparatorRow, createUsageReportTable, formatTotalsRow, formatUsageDataRow, pushBreakdownRows } from '../_table.ts'; 11 | import { 12 | calculateTotals, 13 | createTotalsObject, 14 | getTotalTokens, 15 | } from '../calculate-cost.ts'; 16 | import { loadWeeklyUsageData } from '../data-loader.ts'; 17 | import { detectMismatches, printMismatchReport } from '../debug.ts'; 18 | import { log, logger } from '../logger.ts'; 19 | 20 | export const weeklyCommand = define({ 21 | name: 'weekly', 22 | description: 'Show usage report grouped by week', 23 | args: { 24 | ...sharedArgs, 25 | startOfWeek: { 26 | type: 'enum', 27 | short: 'w', 28 | description: 'Day to start the week on', 29 | default: 'sunday' as const, 30 | choices: WEEK_DAYS, 31 | }, 32 | }, 33 | toKebab: true, 34 | async run(ctx) { 35 | // Load configuration and merge with CLI arguments 36 | const config = loadConfig(ctx.values.config, ctx.values.debug); 37 | const mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug); 38 | 39 | // --jq implies --json 40 | const useJson = Boolean(mergedOptions.json) || mergedOptions.jq != null; 41 | if (useJson) { 42 | logger.level = 0; 43 | } 44 | 45 | const weeklyData = await loadWeeklyUsageData(mergedOptions); 46 | 47 | if (weeklyData.length === 0) { 48 | if (useJson) { 49 | const emptyOutput = { 50 | weekly: [], 51 | totals: { 52 | inputTokens: 0, 53 | outputTokens: 0, 54 | cacheCreationTokens: 0, 55 | cacheReadTokens: 0, 56 | totalTokens: 0, 57 | totalCost: 0, 58 | }, 59 | }; 60 | log(JSON.stringify(emptyOutput, null, 2)); 61 | } 62 | else { 63 | logger.warn('No Claude usage data found.'); 64 | } 65 | process.exit(0); 66 | } 67 | 68 | // Calculate totals 69 | const totals = calculateTotals(weeklyData); 70 | 71 | // Show debug information if requested 72 | if (mergedOptions.debug && !useJson) { 73 | const mismatchStats = await detectMismatches(undefined); 74 | printMismatchReport(mismatchStats, mergedOptions.debugSamples as number | undefined); 75 | } 76 | 77 | if (useJson) { 78 | // Output JSON format 79 | const jsonOutput = { 80 | weekly: weeklyData.map(data => ({ 81 | week: data.week, 82 | inputTokens: data.inputTokens, 83 | outputTokens: data.outputTokens, 84 | cacheCreationTokens: data.cacheCreationTokens, 85 | cacheReadTokens: data.cacheReadTokens, 86 | totalTokens: getTotalTokens(data), 87 | totalCost: data.totalCost, 88 | modelsUsed: data.modelsUsed, 89 | modelBreakdowns: data.modelBreakdowns, 90 | })), 91 | totals: createTotalsObject(totals), 92 | }; 93 | 94 | // Process with jq if specified 95 | if (mergedOptions.jq != null) { 96 | const jqResult = await processWithJq(jsonOutput, mergedOptions.jq); 97 | if (Result.isFailure(jqResult)) { 98 | logger.error((jqResult.error).message); 99 | process.exit(1); 100 | } 101 | log(jqResult.value); 102 | } 103 | else { 104 | log(JSON.stringify(jsonOutput, null, 2)); 105 | } 106 | } 107 | else { 108 | // Print header 109 | logger.box('Claude Code Token Usage Report - Weekly'); 110 | 111 | // Create table with compact mode support 112 | const tableConfig: UsageReportConfig = { 113 | firstColumnName: 'Week', 114 | dateFormatter: (dateStr: string) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? undefined), 115 | forceCompact: ctx.values.compact, 116 | }; 117 | const table = createUsageReportTable(tableConfig); 118 | 119 | // Add weekly data 120 | for (const data of weeklyData) { 121 | // Main row 122 | const row = formatUsageDataRow(data.week, { 123 | inputTokens: data.inputTokens, 124 | outputTokens: data.outputTokens, 125 | cacheCreationTokens: data.cacheCreationTokens, 126 | cacheReadTokens: data.cacheReadTokens, 127 | totalCost: data.totalCost, 128 | modelsUsed: data.modelsUsed, 129 | }); 130 | table.push(row); 131 | 132 | // Add model breakdown rows if flag is set 133 | if (mergedOptions.breakdown) { 134 | pushBreakdownRows(table, data.modelBreakdowns); 135 | } 136 | } 137 | 138 | // Add empty row for visual separation before totals 139 | addEmptySeparatorRow(table, 8); 140 | 141 | // Add totals 142 | const totalsRow = formatTotalsRow({ 143 | inputTokens: totals.inputTokens, 144 | outputTokens: totals.outputTokens, 145 | cacheCreationTokens: totals.cacheCreationTokens, 146 | cacheReadTokens: totals.cacheReadTokens, 147 | totalCost: totals.totalCost, 148 | }); 149 | table.push(totalsRow); 150 | 151 | log(table.toString()); 152 | 153 | // Show guidance message if in compact mode 154 | if (table.isCompactMode()) { 155 | logger.info('\nRunning in Compact Mode'); 156 | logger.info('Expand terminal width to see cache metrics and total tokens'); 157 | } 158 | } 159 | }, 160 | }); 161 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @fileoverview Main entry point for ccusage CLI tool 5 | * 6 | * This is the main entry point for the ccusage command-line interface tool. 7 | * It provides analysis of Claude Code usage data from local JSONL files. 8 | * 9 | * @module index 10 | */ 11 | 12 | /* eslint-disable antfu/no-top-level-await */ 13 | 14 | import { run } from './commands/index.ts'; 15 | 16 | await run(); 17 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Logging utilities for the ccusage application 3 | * 4 | * This module provides configured logger instances using consola for consistent 5 | * logging throughout the application with package name tagging. 6 | * 7 | * @module logger 8 | */ 9 | 10 | import type { ConsolaInstance } from 'consola'; 11 | import process from 'node:process'; 12 | import { consola } from 'consola'; 13 | 14 | import { name } from '../package.json'; 15 | 16 | /** 17 | * Application logger instance with package name tag 18 | */ 19 | export const logger: ConsolaInstance = consola.withTag(name); 20 | 21 | // Apply LOG_LEVEL environment variable if set 22 | if (process.env.LOG_LEVEL != null) { 23 | const level = Number.parseInt(process.env.LOG_LEVEL, 10); 24 | if (!Number.isNaN(level)) { 25 | logger.level = level; 26 | } 27 | } 28 | 29 | /** 30 | * Direct console.log function for cases where logger formatting is not desired 31 | */ 32 | // eslint-disable-next-line no-console 33 | export const log = console.log; 34 | -------------------------------------------------------------------------------- /test/statusline-test-opus4.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_id": "test-session-opus4", 3 | "transcript_path": "test/test-transcript.jsonl", 4 | "cwd": "/Users/test/project", 5 | "model": { 6 | "id": "claude-opus-4-1-20250805", 7 | "display_name": "Opus 4.1" 8 | }, 9 | "workspace": { 10 | "current_dir": "/Users/test/project", 11 | "project_dir": "/Users/test/project" 12 | }, 13 | "version": "1.0.88", 14 | "output_style": { 15 | "name": "default" 16 | }, 17 | "cost": { 18 | "total_cost_usd": 0.0892, 19 | "total_duration_ms": 180000, 20 | "total_api_duration_ms": 12000, 21 | "total_lines_added": 0, 22 | "total_lines_removed": 0 23 | }, 24 | "exceeds_200k_tokens": false 25 | } 26 | -------------------------------------------------------------------------------- /test/statusline-test-sonnet4.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_id": "test-session-sonnet4", 3 | "transcript_path": "test/test-transcript.jsonl", 4 | "cwd": "/Users/test/project", 5 | "model": { 6 | "id": "claude-sonnet-4-20250514", 7 | "display_name": "Sonnet 4" 8 | }, 9 | "workspace": { 10 | "current_dir": "/Users/test/project", 11 | "project_dir": "/Users/test/project" 12 | }, 13 | "version": "1.0.88", 14 | "output_style": { 15 | "name": "default" 16 | }, 17 | "cost": { 18 | "total_cost_usd": 0.0245, 19 | "total_duration_ms": 120000, 20 | "total_api_duration_ms": 8500, 21 | "total_lines_added": 0, 22 | "total_lines_removed": 0 23 | }, 24 | "exceeds_200k_tokens": false 25 | } 26 | -------------------------------------------------------------------------------- /test/statusline-test-sonnet41.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_id": "test-session-sonnet41", 3 | "transcript_path": "test/test-transcript.jsonl", 4 | "cwd": "/Users/test/project", 5 | "model": { 6 | "id": "claude-sonnet-4-1-20250805", 7 | "display_name": "Sonnet 4.1" 8 | }, 9 | "workspace": { 10 | "current_dir": "/Users/test/project", 11 | "project_dir": "/Users/test/project" 12 | }, 13 | "version": "1.0.88", 14 | "output_style": { 15 | "name": "default" 16 | }, 17 | "cost": { 18 | "total_cost_usd": 0.0356, 19 | "total_duration_ms": 150000, 20 | "total_api_duration_ms": 9500, 21 | "total_lines_added": 0, 22 | "total_lines_removed": 0 23 | }, 24 | "exceeds_200k_tokens": false 25 | } 26 | -------------------------------------------------------------------------------- /test/statusline-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_id": "73cc9f9a-2775-4418-beec-bc36b62a1c6f", 3 | "transcript_path": "/Users/ryoppippi/.config/claude/projects/-Users-ryoppippi-ghq-github-com-ryoppippi-ccusage/73cc9f9a-2775-4418-beec-bc36b62a1c6f.jsonl", 4 | "cwd": "/Users/ryoppippi/ghq/github.com/ryoppippi/ccusage", 5 | "model": { 6 | "id": "claude-sonnet-4-20250514", 7 | "display_name": "Sonnet 4" 8 | }, 9 | "workspace": { 10 | "current_dir": "/Users/ryoppippi/ghq/github.com/ryoppippi/ccusage", 11 | "project_dir": "/Users/ryoppippi/ghq/github.com/ryoppippi/ccusage" 12 | }, 13 | "version": "1.0.88", 14 | "output_style": { 15 | "name": "default" 16 | }, 17 | "cost": { 18 | "total_cost_usd": 0.056266149999999994, 19 | "total_duration_ms": 164055, 20 | "total_api_duration_ms": 13577, 21 | "total_lines_added": 0, 22 | "total_lines_removed": 0 23 | }, 24 | "exceeds_200k_tokens": false 25 | } 26 | -------------------------------------------------------------------------------- /test/test-transcript.jsonl: -------------------------------------------------------------------------------- 1 | {"type":"user","message":{}} 2 | {"type":"assistant","message":{"usage":{"input_tokens":1000,"output_tokens":50,"cache_creation_input_tokens":100,"cache_read_input_tokens":500}}} 3 | {"type":"user","message":{}} 4 | {"type":"assistant","message":{"usage":{"input_tokens":2000,"output_tokens":100,"cache_creation_input_tokens":200,"cache_read_input_tokens":800}}} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "react-jsx", 5 | // Environment setup & latest features 6 | "lib": [ 7 | "ESNext" 8 | ], 9 | "moduleDetection": "force", 10 | "module": "Preserve", 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "types": [ 15 | "vitest/globals", 16 | "vitest/importMeta" 17 | ], 18 | "allowImportingTsExtensions": true, 19 | "allowJs": true, 20 | // Best practices 21 | "strict": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noImplicitOverride": true, 24 | "noPropertyAccessFromIndexSignature": false, 25 | "noUncheckedIndexedAccess": true, 26 | // Some stricter flags (disabled by default) 27 | "noUnusedLocals": false, 28 | "noUnusedParameters": false, 29 | "noEmit": true, 30 | "verbatimModuleSyntax": true, 31 | "erasableSyntaxOnly": true, 32 | "skipLibCheck": true 33 | }, 34 | "exclude": [ 35 | "dist" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown'; 2 | import Macros from 'unplugin-macros/rolldown'; 3 | 4 | export default defineConfig({ 5 | entry: [ 6 | './src/*.ts', 7 | '!./src/**/*.test.ts', // Exclude test files 8 | '!./src/_*.ts', // Exclude internal files with underscore prefix 9 | ], 10 | outDir: 'dist', 11 | format: 'esm', 12 | clean: true, 13 | sourcemap: false, 14 | minify: 'dce-only', 15 | treeshake: true, 16 | dts: { 17 | tsgo: true, 18 | resolve: ['type-fest'], 19 | }, 20 | publint: true, 21 | unused: true, 22 | exports: true, 23 | nodeProtocol: true, 24 | plugins: [ 25 | Macros({ 26 | include: ['src/index.ts', 'src/pricing-fetcher.ts'], 27 | }), 28 | ], 29 | define: { 30 | 'import.meta.vitest': 'undefined', 31 | }, 32 | onSuccess: 'sort-package-json', 33 | }); 34 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | locale = 'en-us' 3 | extend-ignore-re = [ 4 | "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", 5 | "(?s)", 6 | "(?Rm)^.*#\\s*spellchecker:disable-line$", 7 | "(?m)^.*\\n.*$" 8 | ] 9 | 10 | [default.extend-words] 11 | color = "color" 12 | 13 | [files] 14 | extend-exclude = [ 15 | "node_modules", 16 | "biome.json" 17 | ] 18 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import Macros from 'unplugin-macros/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | watch: false, 7 | includeSource: ['src/**/*.{js,ts}'], 8 | globals: true, 9 | }, 10 | plugins: [ 11 | Macros({ 12 | include: ['src/index.ts', 'src/pricing-fetcher.ts'], 13 | }), 14 | ], 15 | }); 16 | --------------------------------------------------------------------------------