Showing preview only (991K chars total). Download the full file or copy to clipboard to get everything.
Repository: ryoppippi/ccusage
Branch: main
Commit: 2d4222c35067
Files: 211
Total size: 934.0 KB
Directory structure:
gitextract_lintubch/
├── .claude/
│ ├── commands/
│ │ └── reduce-similarities.md
│ └── skills/
│ ├── byethrow/
│ │ └── SKILL.md
│ └── use-gunshi-cli/
│ └── SKILL.md
├── .envrc
├── .githooks/
│ └── pre-commit
├── .github/
│ ├── FUNDING.yaml
│ ├── actions/
│ │ └── setup-nix/
│ │ └── action.yaml
│ ├── renovate.json
│ └── workflows/
│ ├── check-pr-title.yaml
│ ├── ci.yaml
│ └── release.yaml
├── .gitignore
├── .mcp.json
├── .oxfmtrc.jsonc
├── CLAUDE.md
├── apps/
│ ├── amp/
│ │ ├── CLAUDE.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _consts.ts
│ │ │ ├── _macro.ts
│ │ │ ├── _types.ts
│ │ │ ├── commands/
│ │ │ │ ├── daily.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── monthly.ts
│ │ │ │ └── session.ts
│ │ │ ├── data-loader.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.ts
│ │ │ ├── pricing.ts
│ │ │ └── run.ts
│ │ ├── tsconfig.json
│ │ ├── tsdown.config.ts
│ │ └── vitest.config.ts
│ ├── ccusage/
│ │ ├── CLAUDE.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── config-schema.json
│ │ ├── 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
│ │ │ ├── _macro.ts
│ │ │ ├── _pricing-fetcher.ts
│ │ │ ├── _project-names.ts
│ │ │ ├── _session-blocks.ts
│ │ │ ├── _shared-args.ts
│ │ │ ├── _token-utils.ts
│ │ │ ├── _types.ts
│ │ │ ├── _utils.ts
│ │ │ ├── calculate-cost.ts
│ │ │ ├── commands/
│ │ │ │ ├── _session_id.ts
│ │ │ │ ├── blocks.ts
│ │ │ │ ├── daily.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── monthly.ts
│ │ │ │ ├── session.ts
│ │ │ │ ├── statusline.ts
│ │ │ │ └── weekly.ts
│ │ │ ├── data-loader.ts
│ │ │ ├── debug.ts
│ │ │ ├── index.ts
│ │ │ └── logger.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
│ │ └── vitest.config.ts
│ ├── codex/
│ │ ├── CLAUDE.md
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _consts.ts
│ │ │ ├── _macro.ts
│ │ │ ├── _shared-args.ts
│ │ │ ├── _types.ts
│ │ │ ├── command-utils.ts
│ │ │ ├── commands/
│ │ │ │ ├── daily.ts
│ │ │ │ ├── monthly.ts
│ │ │ │ └── session.ts
│ │ │ ├── daily-report.ts
│ │ │ ├── data-loader.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.ts
│ │ │ ├── monthly-report.ts
│ │ │ ├── pricing.ts
│ │ │ ├── run.ts
│ │ │ ├── session-report.ts
│ │ │ └── token-utils.ts
│ │ ├── tsconfig.json
│ │ ├── tsdown.config.ts
│ │ └── vitest.config.ts
│ ├── mcp/
│ │ ├── CLAUDE.md
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── ccusage.ts
│ │ │ ├── cli-utils.ts
│ │ │ ├── codex.ts
│ │ │ ├── command.ts
│ │ │ ├── consts.ts
│ │ │ ├── index.ts
│ │ │ ├── mcp-utils.ts
│ │ │ └── mcp.ts
│ │ ├── tsconfig.json
│ │ ├── tsdown.config.ts
│ │ └── vitest.config.ts
│ ├── opencode/
│ │ ├── CLAUDE.md
│ │ ├── README.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── commands/
│ │ │ │ ├── daily.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── monthly.ts
│ │ │ │ ├── session.ts
│ │ │ │ └── weekly.ts
│ │ │ ├── cost-utils.ts
│ │ │ ├── data-loader.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.ts
│ │ │ └── run.ts
│ │ ├── tsconfig.json
│ │ ├── tsdown.config.ts
│ │ └── vitest.config.ts
│ └── pi/
│ ├── CLAUDE.md
│ ├── README.md
│ ├── eslint.config.js
│ ├── package.json
│ ├── src/
│ │ ├── _consts.ts
│ │ ├── _pi-agent.ts
│ │ ├── _types.ts
│ │ ├── commands/
│ │ │ ├── daily.ts
│ │ │ ├── index.ts
│ │ │ ├── monthly.ts
│ │ │ └── session.ts
│ │ ├── data-loader.ts
│ │ ├── index.ts
│ │ └── logger.ts
│ ├── tsconfig.json
│ ├── tsdown.config.ts
│ └── vitest.config.ts
├── ccusage.example.json
├── docs/
│ ├── .gitignore
│ ├── .vitepress/
│ │ └── config.ts
│ ├── CLAUDE.md
│ ├── eslint.config.js
│ ├── guide/
│ │ ├── blocks-reports.md
│ │ ├── cli-options.md
│ │ ├── codex/
│ │ │ ├── daily.md
│ │ │ ├── index.md
│ │ │ ├── monthly.md
│ │ │ └── session.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
│ │ ├── opencode/
│ │ │ └── index.md
│ │ ├── pi/
│ │ │ └── index.md
│ │ ├── related-projects.md
│ │ ├── session-reports.md
│ │ ├── sponsors.md
│ │ ├── statusline.md
│ │ └── weekly-reports.md
│ ├── index.md
│ ├── package.json
│ ├── public/
│ │ └── mcp-claude-desktop.avif
│ ├── tsconfig.json
│ ├── typedoc.config.ts
│ ├── update-api-index.ts
│ └── wrangler.jsonc
├── eslint.config.js
├── flake.nix
├── package.json
├── packages/
│ ├── internal/
│ │ ├── CLAUDE.md
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── constants.ts
│ │ │ ├── format.ts
│ │ │ ├── logger.ts
│ │ │ ├── pricing-fetch-utils.ts
│ │ │ └── pricing.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── terminal/
│ ├── CLAUDE.md
│ ├── eslint.config.js
│ ├── package.json
│ ├── src/
│ │ ├── table.ts
│ │ └── utils.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── pnpm-workspace.yaml
└── typos.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .claude/commands/reduce-similarities.md
================================================
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.
================================================
FILE: .claude/skills/byethrow/SKILL.md
================================================
---
name: byethrow
description: Reference the byethrow documentation to understand and use the Result type library for error handling in JavaScript/TypeScript. Access detailed API references, practical usage examples, and best practice guides.
allowed-tools: Read, Grep, Glob
---
## About byethrow
`@praha/byethrow` is a lightweight, tree-shakable Result type library for handling fallible operations in JavaScript and TypeScript.
It provides a simple, consistent API for managing errors and results without throwing exceptions.
For detailed API references and usage examples, refer to the documentation in `node_modules/@praha/byethrow-docs/docs/**/*.md`.
### Documentation CLI
The byethrow documentation CLI provides commands to browse, search, and navigate documentation directly from your terminal.
#### `list` command
List all available documentation organized by sections.
```bash
# List all documentation
npx @praha/byethrow-docs list
# List documentation with filter query
npx @praha/byethrow-docs list --query "your query"
```
**Options:**
- `--query <string>`: Filter documentation by keywords (optional)
#### `search` command
Search documentation and get matching results with highlighted snippets.
```bash
# Search documentation
npx @praha/byethrow-docs search "your query"
# Limit number of results (default: 5)
npx @praha/byethrow-docs search "your query" --limit 10
```
**Arguments:**
- `query`: Search query string (required)
**Options:**
- `--limit <number>`: Maximum number of results to return (default: 5)
#### `toc` command
Display table of contents from a documentation file.
```bash
# Display table of contents from a markdown file
npx @praha/byethrow-docs toc path/to/document.md
```
**Arguments:**
- `path`: Path to the documentation file (required)
================================================
FILE: .claude/skills/use-gunshi-cli/SKILL.md
================================================
---
name: use-gunshi-cli
description: Use the Gunshi library to create command-line interfaces in JavaScript/TypeScript.
globs: '*.ts, *.tsx, *.js, *.jsx, package.json'
alwaysApply: false
---
use gunshi library for creating cli instead of other libraries including cac, yargs, commander, etc.
Gunshi is a modern javascript command-line library
For more information, read the gunshi API docs in `node_modules/@gunshi/docs/**.md`.
================================================
FILE: .envrc
================================================
watch_file pnpm-lock.yaml
watch_file pnpm-workspace.yaml
use flake
================================================
FILE: .githooks/pre-commit
================================================
#!/bin/sh
# Run lint-staged
npx --no-install lint-staged
================================================
FILE: .github/FUNDING.yaml
================================================
github: ryoppippi
================================================
FILE: .github/actions/setup-nix/action.yaml
================================================
name: Setup Nix
description: Install Nix and configure Cachix
inputs:
cachix-auth-token:
description: Cachix authentication token
required: false
runs:
using: composite
steps:
- name: Install Nix
uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
with:
github_access_token: ${{ github.token }}
- name: Setup Cachix (numtide)
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: numtide
authToken: ''
- name: Setup Cachix (ryoppippi)
if: inputs.cachix-auth-token != ''
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ryoppippi
authToken: ${{ inputs.cachix-auth-token }}
- name: Load Nix development environment
shell: bash
run: nix develop --command true
================================================
FILE: .github/renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"nix": {
"enabled": true
},
"extends": ["github>ryoppippi/renovate-config:no-group"]
}
================================================
FILE: .github/workflows/check-pr-title.yaml
================================================
name: Check PR title
on:
pull_request:
types:
- opened
- reopened
- edited
- synchronize
permissions:
pull-requests: read
jobs:
main:
name: Validate PR title
runs-on: ubuntu-slim
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
ignoreLabels: |
autorelease: pending
dependencies
================================================
FILE: .github/workflows/ci.yaml
================================================
name: CI
on:
push:
pull_request:
jobs:
lint-check:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-nix
- run: nix develop --command pnpm lint
- run: nix develop --command pnpm typecheck
test:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-nix
- name: Create default Claude directories for tests
run: |
mkdir -p $HOME/.claude/projects
mkdir -p $HOME/.config/claude/projects
- run: nix develop --command pnpm run test
npm-publish-dry-run-and-upload-pkg-pr-now:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-nix
- run: nix develop --command pnpm pkg-pr-new publish --pnpm './apps/*'
spell-check:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-nix
- run: nix develop --command typos --config ./typos.toml
schema-check:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-nix
- name: Generate schema files
run: nix develop --command pnpm run generate:schema
working-directory: apps/ccusage
- name: Check if schema files are up-to-date
run: |
if git diff --exit-code apps/ccusage/config-schema.json docs/public/config-schema.json; then
echo "✅ Schema files are up-to-date"
else
echo "❌ Schema files are not up-to-date. Please run 'pnpm run generate:schema' and commit the changes."
echo ""
echo "Changed files:"
git diff --name-only apps/ccusage/config-schema.json docs/public/config-schema.json
echo ""
echo "Diff:"
git diff apps/ccusage/config-schema.json docs/public/config-schema.json
exit 1
fi
================================================
FILE: .github/workflows/release.yaml
================================================
name: npm publish
on:
push:
tags:
- '*'
jobs:
npm:
runs-on: ubuntu-24.04-arm
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/setup-nix
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
registry-url: 'https://registry.npmjs.org'
node-version: lts/*
- run: nix develop --command pnpm --filter='./apps/**' publish --provenance --no-git-checks --access public
release:
needs:
- npm
runs-on: ubuntu-24.04-arm
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/setup-nix
- run: nix develop --command pnpm changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
================================================
FILE: .gitignore
================================================
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
.eslintcache
# nix
.direnv
!.envrc
================================================
FILE: .mcp.json
================================================
{
"mcpServers": {
"context7": {
"type": "http",
"url": "https://mcp.context7.com/mcp"
},
"grep": {
"type": "http",
"url": "https://mcp.grep.app"
}
}
}
================================================
FILE: .oxfmtrc.jsonc
================================================
{
"$schema": "https://unpkg.com/oxfmt/configuration_schema.json",
"useTabs": true,
"singleQuote": true,
"files": {
"ignore": ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/pnpm-lock.yaml"],
},
}
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Monorepo Structure
This is a monorepo containing multiple packages. For package-specific guidance, refer to the individual CLAUDE.md files:
- **Main CLI Package**: @apps/ccusage/CLAUDE.md - Core ccusage CLI tool and library
- **Codex CLI Package**: @apps/codex/CLAUDE.md - OpenAI Codex usage tracking CLI
- **OpenCode CLI Package**: @apps/opencode/CLAUDE.md - OpenCode usage tracking CLI
- **MCP Server Package**: @apps/mcp/CLAUDE.md - MCP server implementation for ccusage data
- **Documentation**: @docs/CLAUDE.md - VitePress-based documentation website
Each package has its own development commands, dependencies, and specific guidelines. Always check the relevant package's CLAUDE.md when working within that package directory.
### Apps Are Bundled
All projects under `apps/` ship as bundled CLIs/binaries. Treat their runtime dependencies as bundled assets: list everything in each app's `devDependencies` (never `dependencies`) so the bundler owns the runtime payload.
## Development Commands
**Testing and Quality:**
- `pnpm run test` - Run all tests (using vitest via pnpm, watch mode disabled)
- Lint code using ESLint MCP server (available via Claude Code tools)
- `pnpm run format` - Format code with ESLint (writes changes)
- `pnpm typecheck` - Type check with TypeScript
**Build and Release:**
- `pnpm run build` - Build distribution files with tsdown
- `pnpm run release` - Full release workflow (lint + typecheck + test + build + version bump)
**Development Usage:**
- `pnpm run start daily` - Show daily usage report
- `pnpm run start monthly` - Show monthly usage report
- `pnpm run start session` - Show session-based usage report
- `pnpm run start blocks` - Show 5-hour billing blocks usage report
- `pnpm run start statusline` - Show compact status line (Beta)
- `pnpm run start daily --json` - Show daily usage report in JSON format
- `pnpm run start monthly --json` - Show monthly usage report in JSON format
- `pnpm run start session --json` - Show session usage report in JSON format
- `pnpm run start blocks --json` - Show blocks usage report in JSON format
- `pnpm run start daily --mode <mode>` - Control cost calculation mode (auto/calculate/display)
- `pnpm run start monthly --mode <mode>` - Control cost calculation mode (auto/calculate/display)
- `pnpm run start session --mode <mode>` - Control cost calculation mode (auto/calculate/display)
- `pnpm run start blocks --mode <mode>` - Control cost calculation mode (auto/calculate/display)
- `pnpm run start blocks --active` - Show only active block with projections
- `pnpm run start blocks --recent` - Show blocks from last 3 days (including active)
- `pnpm run start blocks --token-limit <limit>` - Token limit for quota warnings (number or "max")
- `node ./src/index.ts` - Direct execution for development
**MCP Server Usage:** (now provided by the `@ccusage/mcp` package)
- `pnpm dlx @ccusage/mcp@latest -- --help` - Show available options
- `pnpm dlx @ccusage/mcp@latest -- --type http --port 8080` - Start HTTP transport
**Cost Calculation Modes:**
- `auto` (default) - Use pre-calculated costUSD when available, otherwise calculate from tokens
- `calculate` - Always calculate costs from token counts using model pricing, ignore costUSD
- `display` - Always use pre-calculated costUSD values, show 0 for missing costs
**Environment Variables:**
- `LOG_LEVEL` - Control logging verbosity (0=silent, 1=warn, 2=log, 3=info, 4=debug, 5=trace)
- Example: `LOG_LEVEL=0 pnpm run start daily` for silent output
- Useful for debugging or suppressing non-critical output
**Multiple Claude Data Directories:**
This tool supports multiple Claude data directories to handle different Claude Code installations:
- **Default Behavior**: Automatically searches both `~/.config/claude/projects/` (new default) and `~/.claude/projects/` (old default)
- **Environment Variable**: Set `CLAUDE_CONFIG_DIR` to specify custom path(s)
- Single path: `export CLAUDE_CONFIG_DIR="/path/to/claude"`
- Multiple paths: `export CLAUDE_CONFIG_DIR="/path/to/claude1,/path/to/claude2"`
- **Data Aggregation**: Usage data from all valid directories is automatically combined
- **Backward Compatibility**: Existing configurations continue to work without changes
This addresses the breaking change in Claude Code where logs moved from `~/.claude` to `~/.config/claude`.
## Architecture Overview
This is a CLI tool that analyzes Claude Code usage data from local JSONL files stored in Claude data directories (supports both `~/.claude/projects/` and `~/.config/claude/projects/`). The architecture follows a clear separation of concerns:
**Core Data Flow:**
1. **Data Loading** (`data-loader.ts`) - Parses JSONL files from multiple Claude data directories, including pre-calculated costs
2. **Token Aggregation** (`calculate-cost.ts`) - Utility functions for aggregating token counts and costs
3. **Command Execution** (`commands/`) - CLI subcommands that orchestrate data loading and presentation
4. **CLI Entry** (`index.ts`) - Gunshi-based CLI setup with subcommand routing
**Output Formats:**
- Table format (default): Pretty-printed tables with colors for terminal display
- JSON format (`--json`): Structured JSON output for programmatic consumption
**Key Data Structures:**
- Raw usage data is parsed from JSONL with timestamp, token counts, and pre-calculated costs
- Data is aggregated into daily summaries, monthly summaries, session summaries, or 5-hour billing blocks
- **Important Note on Naming**: The term "session" in this codebase has two different meanings:
1. **Session Reports** (`pnpm run start session`): Groups usage by project directories. What we call "sessionId" in these reports is actually derived from the directory structure (project/directory)
2. **True Session ID**: The actual Claude Code session ID found in the `sessionId` field within JSONL entries and used as the filename ({sessionId}.jsonl)
- File structure: `projects/{project}/{sessionId}.jsonl` where:
- `{project}` is the project directory name (used for grouping)
- `{sessionId}.jsonl` is the JSONL file named with the actual session ID from Claude Code
- Each JSONL file contains all usage entries for a single Claude Code session
- The sessionId in the filename matches the `sessionId` field inside the JSONL entries
- 5-hour blocks group usage data by Claude's billing cycles with active block tracking
**External Dependencies:**
- Uses local timezone for date formatting
- CLI built with `gunshi` framework, tables with `cli-table3`
- **LiteLLM Integration**: Cost calculations depend on LiteLLM's pricing database for model pricing data
**MCP Integration:**
- **Built-in MCP Server**: Exposes usage data through MCP protocol with tools:
- `daily` - Daily usage reports
- `session` - Session-based usage reports
- `monthly` - Monthly usage reports
- `blocks` - 5-hour billing blocks usage reports
- **External MCP Servers Available:**
- **ESLint MCP**: Lint TypeScript/JavaScript files directly through Claude Code tools
- **Context7 MCP**: Look up documentation for libraries and frameworks
- **Claude Code Skills Available:**
- **use-gunshi-cli**: Guide for using gunshi CLI framework (via @gunshi/docs)
- **byethrow**: Guide for using @praha/byethrow Result type (via @praha/byethrow-docs)
## Git Commit and PR Conventions
**Commit Message Format:**
Follow the Conventional Commits specification with package/area prefixes:
```
<type>(<scope>): <subject>
```
**Scope Naming Rules:**
- **Apps**: Use the app directory name
- `feat(ccusage):` - Changes to apps/ccusage
- `fix(mcp):` - Fixes in apps/mcp
- `feat(codex):` - Features for apps/codex (if exists)
- **Packages**: Use the package directory name
- `feat(terminal):` - Changes to packages/terminal
- `fix(ui):` - Fixes in packages/ui
- `refactor(core):` - Refactoring packages/core
- **Documentation**: Use `docs` scope
- `docs:` or `docs(guide):` - Documentation updates
- `docs(api):` - API documentation changes
- **Root-level changes**: No scope (preferred) or use `root`
- `chore:` - Root config updates
- `ci:` - CI/CD changes
- `feat:` - Root-level features
- `docs:` - Root documentation updates
- `build:` or `build(root):` - Root build system changes
**Type Prefixes:**
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation only changes
- `style:` - Code style changes (formatting, missing semi-colons, etc)
- `refactor:` - Code change that neither fixes a bug nor adds a feature
- `perf:` - Performance improvements
- `test:` - Adding missing tests or correcting existing tests
- `chore:` - Changes to the build process or auxiliary tools
- `ci:` - CI/CD configuration changes
- `revert:` - Reverting a previous commit
**Examples:**
```
feat(ccusage): add support for Claude 4.1 models
fix(mcp): resolve connection timeout issues
docs(guide): update installation instructions
refactor(ccusage): extract cost calculation to separate module
test(mcp): add integration tests for HTTP transport
chore: update dependencies
```
**PR Title Convention:**
PR titles should follow the same format as commit messages. When a PR contains multiple commits, the title should describe the main change:
```
feat(ccusage): implement session-based usage reports
fix(mcp): handle edge cases in data aggregation
docs: comprehensive API documentation update
```
## Code Style Notes
- Uses ESLint for linting and formatting with tab indentation and double quotes
- TypeScript with strict mode and bundler module resolution
- No console.log allowed except where explicitly disabled with eslint-disable
- Error handling: silently skips malformed JSONL lines during parsing
- File paths always use Node.js path utilities for cross-platform compatibility
- **Import conventions**: Use `.ts` extensions for local file imports (e.g., `import { foo } from './utils.ts'`)
**Error Handling:**
- **Prefer @praha/byethrow Result type** over traditional try-catch for functional error handling
- Documentation: Available via byethrow skill (use `/byethrow` or check `.claude/skills/byethrow/`)
- Use `Result.try()` for wrapping operations that may throw (JSON parsing, etc.)
- Use `Result.isFailure()` for checking errors (more readable than `!Result.isSuccess()`)
- Use early return pattern (`if (Result.isFailure(result)) continue;`) instead of ternary operators
- For async operations: create wrapper function with `Result.try()` then call it
- Keep traditional try-catch only for: file I/O with complex error handling, legacy code that's hard to refactor
- Always use `Result.isFailure()` and `Result.isSuccess()` type guards for better code clarity
**Naming Conventions:**
- Variables: start with lowercase (camelCase) - e.g., `usageDataSchema`, `modelBreakdownSchema`
- Types: start with uppercase (PascalCase) - e.g., `UsageData`, `ModelBreakdown`
- Constants: can use UPPER_SNAKE_CASE - e.g., `DEFAULT_CLAUDE_CODE_PATH`
- Internal files: use underscore prefix - e.g., `_types.ts`, `_utils.ts`, `_consts.ts`
**Export Rules:**
- **IMPORTANT**: Only export constants, functions, and types that are actually used by other modules
- Internal/private constants that are only used within the same file should NOT be exported
- Always check if a constant is used elsewhere before making it `export const` vs just `const`
- This follows the principle of minimizing the public API surface area
- Dependencies should always be added as `devDependencies` unless explicitly requested otherwise
**Post-Code Change Workflow:**
After making any code changes, ALWAYS run these commands in parallel:
- `pnpm run format` - Auto-fix and format code with ESLint (includes linting)
- `pnpm typecheck` - Type check with TypeScript
- `pnpm run test` - Run all tests
This ensures code quality and catches issues immediately after changes.
## Documentation Guidelines
**Screenshot Usage:**
- **Placement**: Always place screenshots immediately after the main heading (H1) in documentation pages
- **Purpose**: Provide immediate visual context to users before textual explanations
- **Guides with Screenshots**:
- `/docs/guide/index.md` (What is ccusage) - Main usage screenshot
- `/docs/guide/daily-reports.md` - Daily report output screenshot
- `/docs/guide/live-monitoring.md` - Live monitoring dashboard screenshot
- `/docs/guide/mcp-server.md` - Claude Desktop integration screenshot
- **Image Path**: Use relative paths like `/screenshot.png` for images stored in `/docs/public/`
- **Alt Text**: Always include descriptive alt text for accessibility
## Claude Models and Testing
**Supported Claude 4 Models (as of 2025):**
- `claude-sonnet-4-20250514` - Latest Claude 4 Sonnet model
- `claude-opus-4-20250514` - Latest Claude 4 Opus model
**Model Naming Convention:**
- Pattern: `claude-{model-type}-{generation}-{date}`
- Example: `claude-sonnet-4-20250514` (NOT `claude-4-sonnet-20250514`)
- The generation number comes AFTER the model type
**Testing Guidelines:**
- **In-Source Testing Pattern**: This project uses in-source testing with `if (import.meta.vitest != null)` blocks
- Tests are written directly in the same files as the source code, not in separate test files
- Vitest globals (`describe`, `it`, `expect`) are available automatically without imports
- **IMPORTANT**: DO NOT use `await import()` dynamic imports anywhere in the codebase - this causes tree-shaking issues and should be avoided entirely
- **ESPECIALLY**: Never use dynamic imports in vitest test blocks - this is particularly problematic for test execution
- **Vitest globals are enabled**: Use `describe`, `it`, `expect` directly without any imports since globals are configured
- Mock data is created using `fs-fixture` with `createFixture()` for Claude data directory simulation
- All test files must use current Claude 4 models, not outdated Claude 3 models
- Test coverage should include both Sonnet and Opus models for comprehensive validation
- Model names in tests must exactly match LiteLLM's pricing database entries
- When adding new model tests, verify the model exists in LiteLLM before implementation
- Tests depend on real pricing data from LiteLLM - failures may indicate model availability issues
**LiteLLM Integration Notes:**
- Cost calculations require exact model name matches with LiteLLM's database
- Test failures often indicate model names don't exist in LiteLLM's pricing data
- Future model updates require checking LiteLLM compatibility first
- The application cannot calculate costs for models not supported by LiteLLM
# Tips for Claude Code
- Context7 MCP server available for library documentation lookup
- use-gunshi-cli skill available for gunshi CLI framework documentation
- byethrow skill available for @praha/byethrow Result type documentation
- do not use console.log. use logger.ts instead
- **CRITICAL VITEST REMINDER**: Vitest globals are enabled - use `describe`, `it`, `expect` directly WITHOUT imports. NEVER use `await import()` dynamic imports anywhere, especially in test blocks.
# important-instruction-reminders
Do what has been asked; nothing more, nothing less.
NEVER create files unless they're absolutely necessary for achieving your goal.
ALWAYS prefer editing an existing file to creating a new one.
NEVER proactively create documentation files (\*.md) or README files. Only create documentation files if explicitly requested by the User.
Dependencies should always be added as devDependencies unless explicitly requested otherwise.
================================================
FILE: apps/amp/CLAUDE.md
================================================
# Amp CLI Notes
## Log Sources
- Amp session usage is recorded under `${AMP_DATA_DIR:-~/.local/share/amp}/threads/` (the CLI resolves `AMP_DATA_DIR` and falls back to `~/.local/share/amp`).
- Each thread is stored as a JSON file (not JSONL) named `T-{uuid}.json`.
- Token usage is extracted from the `usageLedger.events[]` array in each thread file.
- Cache token information (creation/read) is extracted from `messages[].usage` for detailed breakdown.
## Token Fields
- `inputTokens`: total input tokens sent to the model.
- `outputTokens`: output tokens (completion text).
- `cacheCreationInputTokens`: tokens used for cache creation (from message usage).
- `cacheReadInputTokens`: tokens read from cache (from message usage).
- `totalTokens`: sum of input and output tokens.
## Credits
- Amp uses a credits-based billing system in addition to standard token counts.
- Each usage event includes a `credits` field representing the billing cost in Amp's credit system.
- Credits are displayed alongside USD cost estimates in reports.
## Cost Calculation
- Pricing is pulled from LiteLLM's public JSON (`model_prices_and_context_window.json`).
- Amp primarily uses Anthropic Claude models (Haiku, Sonnet, Opus variants).
- Cost formula per model:
- Input: `inputTokens / 1_000_000 * input_cost_per_mtoken`
- Cached input read: `cacheReadInputTokens / 1_000_000 * cached_input_cost_per_mtoken`
- Cache creation: `cacheCreationInputTokens / 1_000_000 * cache_creation_cost_per_mtoken`
- Output: `outputTokens / 1_000_000 * output_cost_per_mtoken`
## CLI Usage
- Treat Amp as a sibling to `apps/ccusage`, `apps/codex`, and `apps/opencode`.
- Reuse shared packages (`@ccusage/terminal`, `@ccusage/internal`) wherever possible.
- Amp is packaged as a bundled CLI. Keep every runtime dependency in `devDependencies`.
- Entry point uses Gunshi framework with subcommands: `daily`, `monthly`, `session`.
- Data discovery relies on `AMP_DATA_DIR` environment variable.
- Default path: `~/.local/share/amp`.
## Available Commands
- `ccusage-amp daily` - Show daily usage report
- `ccusage-amp monthly` - Show monthly usage report
- `ccusage-amp session` - Show usage by thread (session)
- Add `--json` flag for JSON output format
- Add `--compact` flag for compact table mode
## Testing Notes
- Tests rely on `fs-fixture` with `using` to ensure cleanup.
- All vitest blocks live alongside implementation files via `if (import.meta.vitest != null)`.
- Vitest globals are enabled - use `describe`, `it`, `expect` directly without imports.
- **CRITICAL**: NEVER use `await import()` dynamic imports anywhere, especially in test blocks.
## Data Structure
Amp thread files have the following structure:
```json
{
"id": "T-{uuid}",
"created": 1700000000000,
"title": "Thread Title",
"messages": [
{
"role": "assistant",
"messageId": 1,
"usage": {
"model": "claude-haiku-4-5-20251001",
"inputTokens": 100,
"outputTokens": 50,
"cacheCreationInputTokens": 500,
"cacheReadInputTokens": 200,
"credits": 1.5
}
}
],
"usageLedger": {
"events": [
{
"id": "event-uuid",
"timestamp": "2025-11-23T10:00:00.000Z",
"model": "claude-haiku-4-5-20251001",
"credits": 1.5,
"tokens": {
"input": 100,
"output": 50
},
"operationType": "inference",
"fromMessageId": 0,
"toMessageId": 1
}
]
}
}
```
## Environment Variables
- `AMP_DATA_DIR` - Custom Amp data directory path (defaults to `~/.local/share/amp`)
- `LOG_LEVEL` - Control logging verbosity (0=silent, 1=warn, 2=log, 3=info, 4=debug, 5=trace)
================================================
FILE: apps/amp/eslint.config.js
================================================
import { ryoppippi } from '@ryoppippi/eslint-config';
/** @type {import('eslint').Linter.FlatConfig[]} */
const config = ryoppippi(
{
type: 'app',
stylistic: false,
},
{
rules: {
'test/no-importing-vitest-globals': 'error',
},
},
);
export default config;
================================================
FILE: apps/amp/package.json
================================================
{
"name": "@ccusage/amp",
"type": "module",
"version": "18.0.10",
"description": "Usage analysis tool for Amp CLI sessions",
"author": "ryoppippi",
"license": "MIT",
"funding": "https://github.com/ryoppippi/ccusage?sponsor=1",
"homepage": "https://github.com/ryoppippi/ccusage#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ryoppippi/ccusage.git",
"directory": "apps/amp"
},
"bugs": {
"url": "https://github.com/ryoppippi/ccusage/issues"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"bin": {
"ccusage-amp": "./src/index.ts"
},
"files": [
"dist"
],
"publishConfig": {
"bin": {
"ccusage-amp": "./dist/index.js"
}
},
"engines": {
"node": ">=20.19.4"
},
"scripts": {
"build": "tsdown",
"format": "pnpm run lint --fix",
"lint": "eslint --cache .",
"prepack": "pnpm run build && clean-pkg-json",
"prerelease": "pnpm run lint && pnpm run typecheck && pnpm run build",
"start": "bun ./src/index.ts",
"test": "TZ=UTC vitest",
"typecheck": "tsgo --noEmit"
},
"devDependencies": {
"@ccusage/internal": "workspace:*",
"@ccusage/terminal": "workspace:*",
"@praha/byethrow": "catalog:runtime",
"@ryoppippi/eslint-config": "catalog:lint",
"@typescript/native-preview": "catalog:types",
"clean-pkg-json": "catalog:release",
"eslint": "catalog:lint",
"fast-sort": "catalog:runtime",
"fs-fixture": "catalog:testing",
"gunshi": "catalog:runtime",
"path-type": "catalog:runtime",
"picocolors": "catalog:runtime",
"tinyglobby": "catalog:runtime",
"tsdown": "catalog:build",
"unplugin-macros": "catalog:build",
"unplugin-unused": "catalog:build",
"valibot": "catalog:runtime",
"vitest": "catalog:testing"
}
}
================================================
FILE: apps/amp/src/_consts.ts
================================================
import { homedir } from 'node:os';
import path from 'node:path';
/**
* Environment variable name for custom Amp data directory
*/
export const AMP_DATA_DIR_ENV = 'AMP_DATA_DIR';
/**
* Default Amp data directory path (~/.local/share/amp)
*/
const DEFAULT_AMP_PATH = '.local/share/amp';
/**
* User home directory
*/
const USER_HOME_DIR = homedir();
/**
* Default Amp data directory (absolute path)
*/
export const DEFAULT_AMP_DIR = path.join(USER_HOME_DIR, DEFAULT_AMP_PATH);
/**
* Amp threads subdirectory name
*/
export const AMP_THREADS_DIR_NAME = 'threads';
/**
* Glob pattern for Amp thread files
*/
export const AMP_THREAD_GLOB = '**/*.json';
/**
* Million constant for pricing calculations
*/
export const MILLION = 1_000_000;
================================================
FILE: apps/amp/src/_macro.ts
================================================
import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';
import {
createPricingDataset,
fetchLiteLLMPricingDataset,
filterPricingDataset,
} from '@ccusage/internal/pricing-fetch-utils';
const AMP_MODEL_PREFIXES = ['claude-', 'anthropic/'];
function isAmpModel(modelName: string, _pricing: LiteLLMModelPricing): boolean {
return AMP_MODEL_PREFIXES.some((prefix) => modelName.startsWith(prefix));
}
export async function prefetchAmpPricing(): Promise<Record<string, LiteLLMModelPricing>> {
try {
const dataset = await fetchLiteLLMPricingDataset();
return filterPricingDataset(dataset, isAmpModel);
} catch (error) {
console.warn('Failed to prefetch Amp pricing data, proceeding with empty cache.', error);
return createPricingDataset();
}
}
================================================
FILE: apps/amp/src/_types.ts
================================================
/**
* Token usage delta for a single event
*/
export type TokenUsageDelta = {
inputTokens: number;
cacheCreationInputTokens: number;
cacheReadInputTokens: number;
outputTokens: number;
totalTokens: number;
};
/**
* Token usage event loaded from Amp thread files
*/
export type TokenUsageEvent = TokenUsageDelta & {
timestamp: string;
threadId: string;
model: string;
credits: number;
operationType: string;
};
/**
* Model usage summary with token counts
*/
export type ModelUsage = TokenUsageDelta & {
credits: number;
};
/**
* Daily usage summary
*/
export type DailyUsageSummary = {
date: string;
firstTimestamp: string;
costUSD: number;
credits: number;
models: Map<string, ModelUsage>;
} & TokenUsageDelta;
/**
* Monthly usage summary
*/
export type MonthlyUsageSummary = {
month: string;
firstTimestamp: string;
costUSD: number;
credits: number;
models: Map<string, ModelUsage>;
} & TokenUsageDelta;
/**
* Session (thread) usage summary
*/
export type SessionUsageSummary = {
threadId: string;
title: string;
firstTimestamp: string;
lastTimestamp: string;
costUSD: number;
credits: number;
models: Map<string, ModelUsage>;
} & TokenUsageDelta;
/**
* Model pricing information
*/
export type ModelPricing = {
inputCostPerMToken: number;
cachedInputCostPerMToken: number;
cacheCreationCostPerMToken: number;
outputCostPerMToken: number;
};
/**
* Pricing source interface
*/
export type PricingSource = {
getPricing: (model: string) => Promise<ModelPricing>;
};
/**
* Daily report row for JSON output
*/
export type DailyReportRow = {
date: string;
inputTokens: number;
cacheCreationInputTokens: number;
cacheReadInputTokens: number;
outputTokens: number;
totalTokens: number;
costUSD: number;
credits: number;
models: Record<string, ModelUsage>;
};
/**
* Monthly report row for JSON output
*/
export type MonthlyReportRow = {
month: string;
inputTokens: number;
cacheCreationInputTokens: number;
cacheReadInputTokens: number;
outputTokens: number;
totalTokens: number;
costUSD: number;
credits: number;
models: Record<string, ModelUsage>;
};
/**
* Session report row for JSON output
*/
export type SessionReportRow = {
threadId: string;
title: string;
lastActivity: string;
inputTokens: number;
cacheCreationInputTokens: number;
cacheReadInputTokens: number;
outputTokens: number;
totalTokens: number;
costUSD: number;
credits: number;
models: Record<string, ModelUsage>;
};
================================================
FILE: apps/amp/src/commands/daily.ts
================================================
import type { TokenUsageEvent } from '../_types.ts';
import {
addEmptySeparatorRow,
formatCurrency,
formatDateCompact,
formatModelsDisplayMultiline,
formatNumber,
ResponsiveTable,
} from '@ccusage/terminal/table';
import { define } from 'gunshi';
import pc from 'picocolors';
import { loadAmpUsageEvents } from '../data-loader.ts';
import { AmpPricingSource } from '../pricing.ts';
const TABLE_COLUMN_COUNT = 9;
function groupByDate(events: TokenUsageEvent[]): Map<string, TokenUsageEvent[]> {
const grouped = new Map<string, TokenUsageEvent[]>();
for (const event of events) {
const date = event.timestamp.split('T')[0]!;
const existing = grouped.get(date);
if (existing != null) {
existing.push(event);
} else {
grouped.set(date, [event]);
}
}
return grouped;
}
export const dailyCommand = define({
name: 'daily',
description: 'Show Amp token usage grouped by day',
args: {
json: {
type: 'boolean',
short: 'j',
description: 'Output in JSON format',
},
compact: {
type: 'boolean',
description: 'Force compact table mode',
},
},
async run(ctx) {
const jsonOutput = Boolean(ctx.values.json);
const { events } = await loadAmpUsageEvents();
if (events.length === 0) {
const output = jsonOutput
? JSON.stringify({ daily: [], totals: null })
: 'No Amp usage data found.';
// eslint-disable-next-line no-console
console.log(output);
return;
}
using pricingSource = new AmpPricingSource({ offline: false });
const eventsByDate = groupByDate(events);
const dailyData: Array<{
date: string;
inputTokens: number;
outputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
totalTokens: number;
credits: number;
totalCost: number;
modelsUsed: string[];
}> = [];
for (const [date, dayEvents] of eventsByDate) {
let inputTokens = 0;
let outputTokens = 0;
let cacheCreationTokens = 0;
let cacheReadTokens = 0;
let credits = 0;
let totalCost = 0;
const modelsSet = new Set<string>();
for (const event of dayEvents) {
inputTokens += event.inputTokens;
outputTokens += event.outputTokens;
cacheCreationTokens += event.cacheCreationInputTokens;
cacheReadTokens += event.cacheReadInputTokens;
credits += event.credits;
const cost = await pricingSource.calculateCost(event.model, {
inputTokens: event.inputTokens,
outputTokens: event.outputTokens,
cacheCreationInputTokens: event.cacheCreationInputTokens,
cacheReadInputTokens: event.cacheReadInputTokens,
});
totalCost += cost;
modelsSet.add(event.model);
}
const totalTokens = inputTokens + outputTokens;
dailyData.push({
date,
inputTokens,
outputTokens,
cacheCreationTokens,
cacheReadTokens,
totalTokens,
credits,
totalCost,
modelsUsed: Array.from(modelsSet),
});
}
dailyData.sort((a, b) => a.date.localeCompare(b.date));
const totals = {
inputTokens: dailyData.reduce((sum, d) => sum + d.inputTokens, 0),
outputTokens: dailyData.reduce((sum, d) => sum + d.outputTokens, 0),
cacheCreationTokens: dailyData.reduce((sum, d) => sum + d.cacheCreationTokens, 0),
cacheReadTokens: dailyData.reduce((sum, d) => sum + d.cacheReadTokens, 0),
totalTokens: dailyData.reduce((sum, d) => sum + d.totalTokens, 0),
credits: dailyData.reduce((sum, d) => sum + d.credits, 0),
totalCost: dailyData.reduce((sum, d) => sum + d.totalCost, 0),
};
if (jsonOutput) {
// eslint-disable-next-line no-console
console.log(
JSON.stringify(
{
daily: dailyData,
totals,
},
null,
2,
),
);
return;
}
// eslint-disable-next-line no-console
console.log('\n📊 Amp Token Usage Report - Daily\n');
const table: ResponsiveTable = new ResponsiveTable({
head: [
'Date',
'Models',
'Input',
'Output',
'Cache Create',
'Cache Read',
'Total Tokens',
'Credits',
'Cost (USD)',
],
colAligns: ['left', 'left', 'right', 'right', 'right', 'right', 'right', 'right', 'right'],
compactHead: ['Date', 'Models', 'Input', 'Output', 'Credits', 'Cost (USD)'],
compactColAligns: ['left', 'left', 'right', 'right', 'right', 'right'],
compactThreshold: 100,
forceCompact: Boolean(ctx.values.compact),
style: { head: ['cyan'] },
dateFormatter: (dateStr: string) => formatDateCompact(dateStr),
});
for (const data of dailyData) {
table.push([
data.date,
formatModelsDisplayMultiline(data.modelsUsed),
formatNumber(data.inputTokens),
formatNumber(data.outputTokens),
formatNumber(data.cacheCreationTokens),
formatNumber(data.cacheReadTokens),
formatNumber(data.totalTokens),
data.credits.toFixed(2),
formatCurrency(data.totalCost),
]);
}
addEmptySeparatorRow(table, TABLE_COLUMN_COUNT);
table.push([
pc.yellow('Total'),
'',
pc.yellow(formatNumber(totals.inputTokens)),
pc.yellow(formatNumber(totals.outputTokens)),
pc.yellow(formatNumber(totals.cacheCreationTokens)),
pc.yellow(formatNumber(totals.cacheReadTokens)),
pc.yellow(formatNumber(totals.totalTokens)),
pc.yellow(totals.credits.toFixed(2)),
pc.yellow(formatCurrency(totals.totalCost)),
]);
// eslint-disable-next-line no-console
console.log(table.toString());
if (table.isCompactMode()) {
// eslint-disable-next-line no-console
console.log('\nRunning in Compact Mode');
// eslint-disable-next-line no-console
console.log('Expand terminal width to see cache metrics and total tokens');
}
},
});
================================================
FILE: apps/amp/src/commands/index.ts
================================================
export { dailyCommand } from './daily.ts';
export { monthlyCommand } from './monthly.ts';
export { sessionCommand } from './session.ts';
================================================
FILE: apps/amp/src/commands/monthly.ts
================================================
import type { TokenUsageEvent } from '../_types.ts';
import {
addEmptySeparatorRow,
formatCurrency,
formatDateCompact,
formatModelsDisplayMultiline,
formatNumber,
ResponsiveTable,
} from '@ccusage/terminal/table';
import { define } from 'gunshi';
import pc from 'picocolors';
import { loadAmpUsageEvents } from '../data-loader.ts';
import { AmpPricingSource } from '../pricing.ts';
const TABLE_COLUMN_COUNT = 9;
function groupByMonth(events: TokenUsageEvent[]): Map<string, TokenUsageEvent[]> {
const grouped = new Map<string, TokenUsageEvent[]>();
for (const event of events) {
const month = event.timestamp.slice(0, 7); // YYYY-MM
const existing = grouped.get(month);
if (existing != null) {
existing.push(event);
} else {
grouped.set(month, [event]);
}
}
return grouped;
}
export const monthlyCommand = define({
name: 'monthly',
description: 'Show Amp token usage grouped by month',
args: {
json: {
type: 'boolean',
short: 'j',
description: 'Output in JSON format',
},
compact: {
type: 'boolean',
description: 'Force compact table mode',
},
},
async run(ctx) {
const jsonOutput = Boolean(ctx.values.json);
const { events } = await loadAmpUsageEvents();
if (events.length === 0) {
const output = jsonOutput
? JSON.stringify({ monthly: [], totals: null })
: 'No Amp usage data found.';
// eslint-disable-next-line no-console
console.log(output);
return;
}
using pricingSource = new AmpPricingSource({ offline: false });
const eventsByMonth = groupByMonth(events);
const monthlyData: Array<{
month: string;
inputTokens: number;
outputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
totalTokens: number;
credits: number;
totalCost: number;
modelsUsed: string[];
}> = [];
for (const [month, monthEvents] of eventsByMonth) {
let inputTokens = 0;
let outputTokens = 0;
let cacheCreationTokens = 0;
let cacheReadTokens = 0;
let credits = 0;
let totalCost = 0;
const modelsSet = new Set<string>();
for (const event of monthEvents) {
inputTokens += event.inputTokens;
outputTokens += event.outputTokens;
cacheCreationTokens += event.cacheCreationInputTokens;
cacheReadTokens += event.cacheReadInputTokens;
credits += event.credits;
const cost = await pricingSource.calculateCost(event.model, {
inputTokens: event.inputTokens,
outputTokens: event.outputTokens,
cacheCreationInputTokens: event.cacheCreationInputTokens,
cacheReadInputTokens: event.cacheReadInputTokens,
});
totalCost += cost;
modelsSet.add(event.model);
}
const totalTokens = inputTokens + outputTokens;
monthlyData.push({
month,
inputTokens,
outputTokens,
cacheCreationTokens,
cacheReadTokens,
totalTokens,
credits,
totalCost,
modelsUsed: Array.from(modelsSet),
});
}
monthlyData.sort((a, b) => a.month.localeCompare(b.month));
const totals = {
inputTokens: monthlyData.reduce((sum, d) => sum + d.inputTokens, 0),
outputTokens: monthlyData.reduce((sum, d) => sum + d.outputTokens, 0),
cacheCreationTokens: monthlyData.reduce((sum, d) => sum + d.cacheCreationTokens, 0),
cacheReadTokens: monthlyData.reduce((sum, d) => sum + d.cacheReadTokens, 0),
totalTokens: monthlyData.reduce((sum, d) => sum + d.totalTokens, 0),
credits: monthlyData.reduce((sum, d) => sum + d.credits, 0),
totalCost: monthlyData.reduce((sum, d) => sum + d.totalCost, 0),
};
if (jsonOutput) {
// eslint-disable-next-line no-console
console.log(
JSON.stringify(
{
monthly: monthlyData,
totals,
},
null,
2,
),
);
return;
}
// eslint-disable-next-line no-console
console.log('\n📊 Amp Token Usage Report - Monthly\n');
const table: ResponsiveTable = new ResponsiveTable({
head: [
'Month',
'Models',
'Input',
'Output',
'Cache Create',
'Cache Read',
'Total Tokens',
'Credits',
'Cost (USD)',
],
colAligns: ['left', 'left', 'right', 'right', 'right', 'right', 'right', 'right', 'right'],
compactHead: ['Month', 'Models', 'Input', 'Output', 'Credits', 'Cost (USD)'],
compactColAligns: ['left', 'left', 'right', 'right', 'right', 'right'],
compactThreshold: 100,
forceCompact: Boolean(ctx.values.compact),
style: { head: ['cyan'] },
dateFormatter: (dateStr: string) => formatDateCompact(dateStr),
});
for (const data of monthlyData) {
table.push([
data.month,
formatModelsDisplayMultiline(data.modelsUsed),
formatNumber(data.inputTokens),
formatNumber(data.outputTokens),
formatNumber(data.cacheCreationTokens),
formatNumber(data.cacheReadTokens),
formatNumber(data.totalTokens),
data.credits.toFixed(2),
formatCurrency(data.totalCost),
]);
}
addEmptySeparatorRow(table, TABLE_COLUMN_COUNT);
table.push([
pc.yellow('Total'),
'',
pc.yellow(formatNumber(totals.inputTokens)),
pc.yellow(formatNumber(totals.outputTokens)),
pc.yellow(formatNumber(totals.cacheCreationTokens)),
pc.yellow(formatNumber(totals.cacheReadTokens)),
pc.yellow(formatNumber(totals.totalTokens)),
pc.yellow(totals.credits.toFixed(2)),
pc.yellow(formatCurrency(totals.totalCost)),
]);
// eslint-disable-next-line no-console
console.log(table.toString());
if (table.isCompactMode()) {
// eslint-disable-next-line no-console
console.log('\nRunning in Compact Mode');
// eslint-disable-next-line no-console
console.log('Expand terminal width to see cache metrics and total tokens');
}
},
});
================================================
FILE: apps/amp/src/commands/session.ts
================================================
import type { TokenUsageEvent } from '../_types.ts';
import {
addEmptySeparatorRow,
formatCurrency,
formatDateCompact,
formatModelsDisplayMultiline,
formatNumber,
ResponsiveTable,
} from '@ccusage/terminal/table';
import { define } from 'gunshi';
import pc from 'picocolors';
import { loadAmpUsageEvents } from '../data-loader.ts';
import { AmpPricingSource } from '../pricing.ts';
const TABLE_COLUMN_COUNT = 9;
function groupByThread(events: TokenUsageEvent[]): Map<string, TokenUsageEvent[]> {
const grouped = new Map<string, TokenUsageEvent[]>();
for (const event of events) {
const existing = grouped.get(event.threadId);
if (existing != null) {
existing.push(event);
} else {
grouped.set(event.threadId, [event]);
}
}
return grouped;
}
export const sessionCommand = define({
name: 'session',
description: 'Show Amp token usage grouped by thread (session)',
args: {
json: {
type: 'boolean',
short: 'j',
description: 'Output in JSON format',
},
compact: {
type: 'boolean',
description: 'Force compact table mode',
},
},
async run(ctx) {
const jsonOutput = Boolean(ctx.values.json);
const { events, threads } = await loadAmpUsageEvents();
if (events.length === 0) {
const output = jsonOutput
? JSON.stringify({ sessions: [], totals: null })
: 'No Amp usage data found.';
// eslint-disable-next-line no-console
console.log(output);
return;
}
using pricingSource = new AmpPricingSource({ offline: false });
const eventsByThread = groupByThread(events);
const sessionData: Array<{
threadId: string;
title: string;
inputTokens: number;
outputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
totalTokens: number;
credits: number;
totalCost: number;
modelsUsed: string[];
lastActivity: string;
}> = [];
for (const [threadId, threadEvents] of eventsByThread) {
let inputTokens = 0;
let outputTokens = 0;
let cacheCreationTokens = 0;
let cacheReadTokens = 0;
let credits = 0;
let totalCost = 0;
const modelsSet = new Set<string>();
let lastActivity = threadEvents[0]!.timestamp;
for (const event of threadEvents) {
inputTokens += event.inputTokens;
outputTokens += event.outputTokens;
cacheCreationTokens += event.cacheCreationInputTokens;
cacheReadTokens += event.cacheReadInputTokens;
credits += event.credits;
const cost = await pricingSource.calculateCost(event.model, {
inputTokens: event.inputTokens,
outputTokens: event.outputTokens,
cacheCreationInputTokens: event.cacheCreationInputTokens,
cacheReadInputTokens: event.cacheReadInputTokens,
});
totalCost += cost;
modelsSet.add(event.model);
if (event.timestamp > lastActivity) {
lastActivity = event.timestamp;
}
}
const totalTokens = inputTokens + outputTokens;
const threadInfo = threads.get(threadId);
sessionData.push({
threadId,
title: threadInfo?.title ?? 'Untitled',
inputTokens,
outputTokens,
cacheCreationTokens,
cacheReadTokens,
totalTokens,
credits,
totalCost,
modelsUsed: Array.from(modelsSet),
lastActivity,
});
}
sessionData.sort((a, b) => a.lastActivity.localeCompare(b.lastActivity));
const totals = {
inputTokens: sessionData.reduce((sum, s) => sum + s.inputTokens, 0),
outputTokens: sessionData.reduce((sum, s) => sum + s.outputTokens, 0),
cacheCreationTokens: sessionData.reduce((sum, s) => sum + s.cacheCreationTokens, 0),
cacheReadTokens: sessionData.reduce((sum, s) => sum + s.cacheReadTokens, 0),
totalTokens: sessionData.reduce((sum, s) => sum + s.totalTokens, 0),
credits: sessionData.reduce((sum, s) => sum + s.credits, 0),
totalCost: sessionData.reduce((sum, s) => sum + s.totalCost, 0),
};
if (jsonOutput) {
// eslint-disable-next-line no-console
console.log(
JSON.stringify(
{
sessions: sessionData,
totals,
},
null,
2,
),
);
return;
}
// eslint-disable-next-line no-console
console.log('\n📊 Amp Token Usage Report - Sessions (Threads)\n');
const table: ResponsiveTable = new ResponsiveTable({
head: [
'Thread',
'Models',
'Input',
'Output',
'Cache Create',
'Cache Read',
'Total Tokens',
'Credits',
'Cost (USD)',
],
colAligns: ['left', 'left', 'right', 'right', 'right', 'right', 'right', 'right', 'right'],
compactHead: ['Thread', 'Models', 'Input', 'Output', 'Credits', 'Cost (USD)'],
compactColAligns: ['left', 'left', 'right', 'right', 'right', 'right'],
compactThreshold: 100,
forceCompact: Boolean(ctx.values.compact),
style: { head: ['cyan'] },
dateFormatter: (dateStr: string) => formatDateCompact(dateStr),
});
for (const data of sessionData) {
// Truncate title for display
const displayTitle = data.title.length > 30 ? `${data.title.slice(0, 27)}...` : data.title;
table.push([
displayTitle,
formatModelsDisplayMultiline(data.modelsUsed),
formatNumber(data.inputTokens),
formatNumber(data.outputTokens),
formatNumber(data.cacheCreationTokens),
formatNumber(data.cacheReadTokens),
formatNumber(data.totalTokens),
data.credits.toFixed(2),
formatCurrency(data.totalCost),
]);
}
addEmptySeparatorRow(table, TABLE_COLUMN_COUNT);
table.push([
pc.yellow('Total'),
'',
pc.yellow(formatNumber(totals.inputTokens)),
pc.yellow(formatNumber(totals.outputTokens)),
pc.yellow(formatNumber(totals.cacheCreationTokens)),
pc.yellow(formatNumber(totals.cacheReadTokens)),
pc.yellow(formatNumber(totals.totalTokens)),
pc.yellow(totals.credits.toFixed(2)),
pc.yellow(formatCurrency(totals.totalCost)),
]);
// eslint-disable-next-line no-console
console.log(table.toString());
if (table.isCompactMode()) {
// eslint-disable-next-line no-console
console.log('\nRunning in Compact Mode');
// eslint-disable-next-line no-console
console.log('Expand terminal width to see cache metrics and total tokens');
}
},
});
================================================
FILE: apps/amp/src/data-loader.ts
================================================
/**
* @fileoverview Data loading utilities for Amp CLI usage analysis
*
* This module provides functions for loading and parsing Amp usage data
* from JSON thread files stored in Amp data directories.
* Amp stores usage data in ~/.local/share/amp/threads/
*
* @module data-loader
*/
import type { TokenUsageEvent } from './_types.ts';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { Result } from '@praha/byethrow';
import { createFixture } from 'fs-fixture';
import { isDirectorySync } from 'path-type';
import { glob } from 'tinyglobby';
import * as v from 'valibot';
import {
AMP_DATA_DIR_ENV,
AMP_THREAD_GLOB,
AMP_THREADS_DIR_NAME,
DEFAULT_AMP_DIR,
} from './_consts.ts';
import { logger } from './logger.ts';
/**
* Amp usageLedger event schema
*/
const usageLedgerEventSchema = v.object({
id: v.string(),
timestamp: v.string(),
model: v.string(),
credits: v.number(),
tokens: v.object({
input: v.optional(v.number()),
output: v.optional(v.number()),
}),
operationType: v.optional(v.string()),
fromMessageId: v.optional(v.number()),
toMessageId: v.optional(v.number()),
});
/**
* Amp message usage schema (for cache tokens)
*/
const messageUsageSchema = v.object({
model: v.optional(v.string()),
inputTokens: v.optional(v.number()),
outputTokens: v.optional(v.number()),
cacheCreationInputTokens: v.optional(v.number()),
cacheReadInputTokens: v.optional(v.number()),
totalInputTokens: v.optional(v.number()),
credits: v.optional(v.number()),
});
/**
* Amp message schema
*/
const messageSchema = v.object({
role: v.string(),
messageId: v.number(),
usage: v.optional(messageUsageSchema),
});
/**
* Amp thread file schema
*/
const threadSchema = v.object({
id: v.string(),
created: v.optional(v.number()),
title: v.optional(v.string()),
messages: v.optional(v.array(messageSchema)),
usageLedger: v.optional(
v.object({
events: v.optional(v.array(usageLedgerEventSchema)),
}),
),
});
type ParsedThread = v.InferOutput<typeof threadSchema>;
type ParsedUsageLedgerEvent = v.InferOutput<typeof usageLedgerEventSchema>;
type ParsedMessage = v.InferOutput<typeof messageSchema>;
/**
* Get Amp data directory
* @returns Path to Amp data directory, or null if not found
*/
export function getAmpPath(): string | null {
// Check environment variable first
const envPath = process.env[AMP_DATA_DIR_ENV];
if (envPath != null && envPath.trim() !== '') {
const normalizedPath = path.resolve(envPath);
if (isDirectorySync(normalizedPath)) {
return normalizedPath;
}
}
// Use default path
if (isDirectorySync(DEFAULT_AMP_DIR)) {
return DEFAULT_AMP_DIR;
}
return null;
}
/**
* Find cache token information from messages for a specific messageId range
*/
function findCacheTokensForEvent(
messages: ParsedMessage[] | undefined,
fromMessageId: number | undefined,
toMessageId: number | undefined,
): { cacheCreationInputTokens: number; cacheReadInputTokens: number } {
if (messages == null || toMessageId == null) {
return { cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
}
// Find the assistant message that corresponds to this event
const message = messages.find((m) => m.role === 'assistant' && m.messageId === toMessageId);
if (message?.usage == null) {
return { cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
}
return {
cacheCreationInputTokens: message.usage.cacheCreationInputTokens ?? 0,
cacheReadInputTokens: message.usage.cacheReadInputTokens ?? 0,
};
}
/**
* Convert usageLedger event to TokenUsageEvent
*/
function convertLedgerEventToUsageEvent(
threadId: string,
event: ParsedUsageLedgerEvent,
messages: ParsedMessage[] | undefined,
): TokenUsageEvent {
const inputTokens = event.tokens.input ?? 0;
const outputTokens = event.tokens.output ?? 0;
const { cacheCreationInputTokens, cacheReadInputTokens } = findCacheTokensForEvent(
messages,
event.fromMessageId,
event.toMessageId,
);
return {
timestamp: event.timestamp,
threadId,
model: event.model,
credits: event.credits,
operationType: event.operationType ?? 'unknown',
inputTokens,
outputTokens,
cacheCreationInputTokens,
cacheReadInputTokens,
totalTokens: inputTokens + outputTokens,
};
}
/**
* Load and parse a single Amp thread file
*/
async function loadThreadFile(filePath: string): Promise<ParsedThread | null> {
const readResult = await Result.try({
try: readFile(filePath, 'utf-8'),
catch: (error) => error,
});
if (Result.isFailure(readResult)) {
logger.debug('Failed to read Amp thread file', { filePath, error: readResult.error });
return null;
}
const parseResult = Result.try({
try: () => JSON.parse(readResult.value) as unknown,
catch: (error) => error,
})();
if (Result.isFailure(parseResult)) {
logger.debug('Failed to parse Amp thread JSON', { filePath, error: parseResult.error });
return null;
}
const validationResult = v.safeParse(threadSchema, parseResult.value);
if (!validationResult.success) {
logger.debug('Failed to validate Amp thread schema', {
filePath,
issues: validationResult.issues,
});
return null;
}
return validationResult.output;
}
export type LoadOptions = {
threadDirs?: string[];
};
export type LoadResult = {
events: TokenUsageEvent[];
threads: Map<string, { title: string; created: number | undefined }>;
missingDirectories: string[];
};
/**
* Load all Amp usage events from thread files
*/
export async function loadAmpUsageEvents(options: LoadOptions = {}): Promise<LoadResult> {
const ampPath = getAmpPath();
const providedDirs =
options.threadDirs != null && options.threadDirs.length > 0
? options.threadDirs.map((dir) => path.resolve(dir))
: undefined;
const defaultThreadsDir = ampPath != null ? path.join(ampPath, AMP_THREADS_DIR_NAME) : null;
const threadDirs = providedDirs ?? (defaultThreadsDir != null ? [defaultThreadsDir] : []);
const events: TokenUsageEvent[] = [];
const threads = new Map<string, { title: string; created: number | undefined }>();
const missingDirectories: string[] = [];
for (const dir of threadDirs) {
if (!isDirectorySync(dir)) {
missingDirectories.push(dir);
continue;
}
const files = await glob(AMP_THREAD_GLOB, {
cwd: dir,
absolute: true,
});
for (const file of files) {
const thread = await loadThreadFile(file);
if (thread == null) {
continue;
}
const threadId = thread.id;
threads.set(threadId, {
title: thread.title ?? 'Untitled',
created: thread.created,
});
const ledgerEvents = thread.usageLedger?.events ?? [];
for (const ledgerEvent of ledgerEvents) {
const event = convertLedgerEventToUsageEvent(threadId, ledgerEvent, thread.messages);
events.push(event);
}
}
}
// Sort events by timestamp
events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
return { events, threads, missingDirectories };
}
if (import.meta.vitest != null) {
describe('loadAmpUsageEvents', () => {
it('parses Amp thread files and extracts usage events', async () => {
const threadData = {
v: 195,
id: 'T-test-thread-123',
created: 1700000000000,
title: 'Test Thread',
messages: [
{
role: 'user',
messageId: 0,
content: [{ type: 'text', text: 'hi' }],
},
{
role: 'assistant',
messageId: 1,
content: [{ type: 'text', text: 'Hello!' }],
usage: {
model: 'claude-haiku-4-5-20251001',
inputTokens: 100,
outputTokens: 50,
cacheCreationInputTokens: 500,
cacheReadInputTokens: 200,
totalInputTokens: 800,
credits: 1.5,
},
},
],
usageLedger: {
events: [
{
id: 'event-1',
timestamp: '2025-11-23T10:00:00.000Z',
model: 'claude-haiku-4-5-20251001',
credits: 1.5,
tokens: {
input: 100,
output: 50,
},
operationType: 'inference',
fromMessageId: 0,
toMessageId: 1,
},
],
},
};
await using fixture = await createFixture({
threads: {
'T-test-thread-123.json': JSON.stringify(threadData),
},
});
const { events, threads, missingDirectories } = await loadAmpUsageEvents({
threadDirs: [fixture.getPath('threads')],
});
expect(missingDirectories).toEqual([]);
expect(events).toHaveLength(1);
const event = events[0]!;
expect(event.threadId).toBe('T-test-thread-123');
expect(event.model).toBe('claude-haiku-4-5-20251001');
expect(event.inputTokens).toBe(100);
expect(event.outputTokens).toBe(50);
expect(event.cacheCreationInputTokens).toBe(500);
expect(event.cacheReadInputTokens).toBe(200);
expect(event.credits).toBe(1.5);
expect(threads.get('T-test-thread-123')).toEqual({
title: 'Test Thread',
created: 1700000000000,
});
});
it('handles missing directories gracefully', async () => {
const { events, missingDirectories } = await loadAmpUsageEvents({
threadDirs: ['/nonexistent/path'],
});
expect(events).toEqual([]);
expect(missingDirectories).toContain(path.resolve('/nonexistent/path'));
});
it('handles malformed JSON gracefully', async () => {
await using fixture = await createFixture({
threads: {
'invalid.json': 'not valid json',
},
});
const { events } = await loadAmpUsageEvents({
threadDirs: [fixture.getPath('threads')],
});
expect(events).toEqual([]);
});
});
}
================================================
FILE: apps/amp/src/index.ts
================================================
#!/usr/bin/env node
import { run } from './run.ts';
// eslint-disable-next-line antfu/no-top-level-await
await run();
================================================
FILE: apps/amp/src/logger.ts
================================================
import { createLogger } from '@ccusage/internal/logger';
export const logger = createLogger('@ccusage/amp');
================================================
FILE: apps/amp/src/pricing.ts
================================================
import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';
import type { ModelPricing, PricingSource } from './_types.ts';
import { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';
import { Result } from '@praha/byethrow';
import { MILLION } from './_consts.ts';
import { prefetchAmpPricing } from './_macro.ts' with { type: 'macro' };
import { logger } from './logger.ts';
const AMP_PROVIDER_PREFIXES = ['anthropic/'];
const ZERO_MODEL_PRICING = {
inputCostPerMToken: 0,
cachedInputCostPerMToken: 0,
cacheCreationCostPerMToken: 0,
outputCostPerMToken: 0,
} as const satisfies ModelPricing;
function toPerMillion(value: number | undefined, fallback?: number): number {
const perToken = value ?? fallback ?? 0;
return perToken * MILLION;
}
export type AmpPricingSourceOptions = {
offline?: boolean;
offlineLoader?: () => Promise<Record<string, LiteLLMModelPricing>>;
};
const PREFETCHED_AMP_PRICING = prefetchAmpPricing();
export class AmpPricingSource implements PricingSource, Disposable {
private readonly fetcher: LiteLLMPricingFetcher;
constructor(options: AmpPricingSourceOptions = {}) {
this.fetcher = new LiteLLMPricingFetcher({
offline: options.offline ?? false,
offlineLoader: options.offlineLoader ?? (async () => PREFETCHED_AMP_PRICING),
logger,
providerPrefixes: AMP_PROVIDER_PREFIXES,
});
}
[Symbol.dispose](): void {
this.fetcher[Symbol.dispose]();
}
async getPricing(model: string): Promise<ModelPricing> {
const directLookup = await this.fetcher.getModelPricing(model);
if (Result.isFailure(directLookup)) {
throw directLookup.error;
}
const pricing = directLookup.value;
if (pricing == null) {
logger.warn(`Pricing not found for model ${model}; defaulting to zero-cost pricing.`);
return ZERO_MODEL_PRICING;
}
return {
inputCostPerMToken: toPerMillion(pricing.input_cost_per_token),
cachedInputCostPerMToken: toPerMillion(
pricing.cache_read_input_token_cost,
pricing.input_cost_per_token,
),
cacheCreationCostPerMToken: toPerMillion(
pricing.cache_creation_input_token_cost,
pricing.input_cost_per_token,
),
outputCostPerMToken: toPerMillion(pricing.output_cost_per_token),
};
}
async calculateCost(
model: string,
tokens: {
inputTokens: number;
outputTokens: number;
cacheCreationInputTokens?: number;
cacheReadInputTokens?: number;
},
): Promise<number> {
const result = await this.fetcher.calculateCostFromTokens(
{
input_tokens: tokens.inputTokens,
output_tokens: tokens.outputTokens,
cache_creation_input_tokens: tokens.cacheCreationInputTokens,
cache_read_input_tokens: tokens.cacheReadInputTokens,
},
model,
);
if (Result.isFailure(result)) {
logger.warn(`Failed to calculate cost for model ${model}:`, result.error);
return 0;
}
return result.value;
}
}
if (import.meta.vitest != null) {
describe('AmpPricingSource', () => {
it('converts LiteLLM pricing to per-million costs', async () => {
using source = new AmpPricingSource({
offline: true,
offlineLoader: async () => ({
'claude-haiku-4-5-20251001': {
input_cost_per_token: 1e-6,
output_cost_per_token: 5e-6,
cache_read_input_token_cost: 1e-7,
cache_creation_input_token_cost: 1.25e-6,
},
}),
});
const pricing = await source.getPricing('claude-haiku-4-5-20251001');
expect(pricing.inputCostPerMToken).toBeCloseTo(1);
expect(pricing.outputCostPerMToken).toBeCloseTo(5);
expect(pricing.cachedInputCostPerMToken).toBeCloseTo(0.1);
expect(pricing.cacheCreationCostPerMToken).toBeCloseTo(1.25);
});
it('calculates cost from tokens', async () => {
using source = new AmpPricingSource({
offline: true,
offlineLoader: async () => ({
'claude-haiku-4-5-20251001': {
input_cost_per_token: 1e-6,
output_cost_per_token: 5e-6,
cache_read_input_token_cost: 1e-7,
cache_creation_input_token_cost: 1.25e-6,
},
}),
});
const cost = await source.calculateCost('claude-haiku-4-5-20251001', {
inputTokens: 1000,
outputTokens: 500,
cacheReadInputTokens: 200,
cacheCreationInputTokens: 100,
});
const expected = 1000 * 1e-6 + 500 * 5e-6 + 200 * 1e-7 + 100 * 1.25e-6;
expect(cost).toBeCloseTo(expected);
});
it('falls back to zero pricing for unknown models', async () => {
using source = new AmpPricingSource({
offline: true,
offlineLoader: async () => ({}),
});
const pricing = await source.getPricing('anthropic/unknown');
expect(pricing).toEqual(ZERO_MODEL_PRICING);
});
});
}
================================================
FILE: apps/amp/src/run.ts
================================================
import process from 'node:process';
import { cli } from 'gunshi';
import { description, name, version } from '../package.json';
import { dailyCommand, monthlyCommand, sessionCommand } from './commands/index.ts';
const subCommands = new Map([
['daily', dailyCommand],
['monthly', monthlyCommand],
['session', sessionCommand],
]);
const mainCommand = dailyCommand;
export async function run(): Promise<void> {
// When invoked through npx, the binary name might be passed as the first argument
// Filter it out if it matches the expected binary name
let args = process.argv.slice(2);
if (args[0] === 'ccusage-amp') {
args = args.slice(1);
}
await cli(args, mainCommand, {
name,
version,
description,
subCommands,
renderHeader: null,
});
}
================================================
FILE: apps/amp/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext"],
"moduleDetection": "force",
"module": "Preserve",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"types": ["vitest/globals", "vitest/importMeta"],
"allowImportingTsExtensions": true,
"allowJs": false,
"strict": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noEmit": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"skipLibCheck": true
},
"exclude": ["dist"]
}
================================================
FILE: apps/amp/tsdown.config.ts
================================================
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
clean: true,
dts: false,
shims: true,
platform: 'node',
target: 'node20',
fixedExtension: false,
});
================================================
FILE: apps/amp/vitest.config.ts
================================================
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
includeSource: ['src/**/*.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
define: {
'import.meta.vitest': 'undefined',
},
});
================================================
FILE: apps/ccusage/CLAUDE.md
================================================
# CLAUDE.md - ccusage Package
This is the main ccusage CLI package that provides usage analysis for Claude Code.
## Package Overview
**Name**: `ccusage`
**Description**: Usage analysis tool for Claude Code
**Type**: CLI tool and library with TypeScript exports
## Development Commands
**Testing and Quality:**
- `pnpm run test` - Run all tests (using vitest via pnpm, watch mode disabled)
- `pnpm run lint` - Lint code using ESLint
- `pnpm run format` - Format and auto-fix code with ESLint
- `pnpm typecheck` - Type check with TypeScript
**Build and Release:**
- `pnpm run build` - Build distribution files with tsdown (includes schema generation)
- `pnpm run generate:schema` - Generate JSON schema for configuration
- `pnpm run prerelease` - Full release workflow (lint + typecheck + build)
**Development Usage:**
- `pnpm run start daily` - Show daily usage report
- `pnpm run start monthly` - Show monthly usage report
- `pnpm run start session` - Show session-based usage report
- `pnpm run start blocks` - Show 5-hour billing blocks usage report
- `pnpm run start statusline` - Show compact status line (Beta)
- Add `--json` flag for JSON output format
- Add `--mode <mode>` for cost calculation control (auto/calculate/display)
- Add `--active` flag for blocks to show only active block with projections
- Add `--recent` flag for blocks to show last 3 days including active
**CLI Testing:**
- `pnpm run test:statusline` - Test statusline with default test data
- `pnpm run test:statusline:all` - Test statusline with all model variants
- `pnpm run test:statusline:sonnet4` - Test with Sonnet 4 data
- `pnpm run test:statusline:opus4` - Test with Opus 4 data
- `pnpm run test:statusline:sonnet41` - Test with Sonnet 4.1 data
## Architecture
This package contains the core ccusage functionality:
**Key Modules:**
- `src/index.ts` - CLI entry point with Gunshi-based command routing
- `src/data-loader.ts` - Parses JSONL files from Claude data directories
- `src/calculate-cost.ts` - Token aggregation and cost calculation utilities
- `src/commands/` - CLI subcommands (daily, monthly, session, blocks, statusline)
- `src/logger.ts` - Logging utilities (use instead of console.log)
**Data Flow:**
1. Loads JSONL files from `~/.claude/projects/` and `~/.config/claude/projects/`
2. Aggregates usage data by time periods or sessions
3. Calculates costs using LiteLLM pricing database
4. Outputs formatted tables or JSON
## Testing Guidelines
- **In-Source Testing**: Tests are written in the same files using `if (import.meta.vitest != null)` blocks
- **Vitest Globals Enabled**: Use `describe`, `it`, `expect` directly without imports
- **Model Testing**: Use current Claude 4 models (sonnet-4, opus-4) in tests
- **Mock Data**: Uses `fs-fixture` with `createFixture()` for Claude data simulation
- **CRITICAL**: NEVER use `await import()` dynamic imports anywhere, especially in test blocks
## Code Style
- **Error Handling**: Prefer `@praha/byethrow Result` type over try-catch for functional error handling
- **Imports**: Use `.ts` extensions for local imports (e.g., `import { foo } from './utils.ts'`)
- **Exports**: Only export what's actually used by other modules
- **Dependencies**: Add as `devDependencies` unless explicitly requested otherwise
- **No console.log**: Use `logger.ts` instead
**Post-Change Workflow:**
Always run these commands in parallel after code changes:
- `pnpm run format` - Auto-fix and format
- `pnpm typecheck` - Type checking
- `pnpm run test` - Run tests
## Environment Variables
- `LOG_LEVEL` - Control logging verbosity (0=silent, 1=warn, 2=log, 3=info, 4=debug, 5=trace)
- `CLAUDE_CONFIG_DIR` - Custom Claude data directory paths (supports multiple comma-separated paths)
## Dependencies
Because `ccusage` is distributed as a bundled CLI, keep all runtime libraries in `devDependencies` so the bundler captures them.
**Key Runtime Dependencies:**
- `gunshi` - CLI framework
- `cli-table3` - Table formatting
- `valibot` - Schema validation
- `@praha/byethrow` - Functional error handling
**Key Dev Dependencies:**
- `vitest` - Testing framework
- `tsdown` - TypeScript build tool
- `eslint` - Linting and formatting
- `fs-fixture` - Test fixture creation
## Package Exports
The package provides multiple exports for library usage:
- `.` - Main CLI entry point
- `./calculate-cost` - Cost calculation utilities
- `./data-loader` - Data loading functions
- `./debug` - Debug utilities
- `./logger` - Logging utilities
- `./pricing-fetcher` - LiteLLM pricing integration
================================================
FILE: apps/ccusage/LICENSE
================================================
MIT License
Copyright (c) 2025 ryoppippi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: apps/ccusage/README.md
================================================
<div align="center">
<img src="https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/logo.svg" alt="ccusage logo" width="256" height="256">
<h1>ccusage</h1>
</div>
<p align="center">
<a href="https://socket.dev/api/npm/package/ccusage"><img src="https://socket.dev/api/badge/npm/package/ccusage" alt="Socket Badge" /></a>
<a href="https://npmjs.com/package/ccusage"><img src="https://img.shields.io/npm/v/ccusage?color=yellow" alt="npm version" /></a>
<a href="https://tanstack.com/stats/npm?packageGroups=%5B%7B%22packages%22:%5B%7B%22name%22:%22ccusage%22%7D%5D%7D%5D&range=30-days&transform=none&binType=daily&showDataMode=all&height=400"><img src="https://img.shields.io/npm/dt/ccusage" alt="NPM Downloads" /></a>
<a href="https://packagephobia.com/result?p=ccusage"><img src="https://packagephobia.com/badge?p=ccusage" alt="install size" /></a>
<a href="https://deepwiki.com/ryoppippi/ccusage"><img src="https://img.shields.io/badge/DeepWiki-ryoppippi%2Fccusage-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==" alt="DeepWiki"></a>
<!-- DeepWiki badge generated by https://deepwiki.ryoppippi.com/ -->
<a href="https://github.com/hesreallyhim/awesome-claude-code"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Claude Code" /></a>
</p>
<div align="center">
<img src="https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/screenshot.png">
</div>
> Analyze your Claude Code token usage and costs from local JSONL files — incredibly fast and informative!
## ccusage Family
### 📊 [ccusage](https://www.npmjs.com/package/ccusage) - Claude Code Usage Analyzer
The main CLI tool for analyzing Claude Code usage from local JSONL files. Track daily, monthly, and session-based usage with beautiful tables.
### 🤖 [@ccusage/codex](https://www.npmjs.com/package/@ccusage/codex) - OpenAI Codex Usage Analyzer
Companion tool for analyzing OpenAI Codex usage. Same powerful features as ccusage but tailored for Codex users, including GPT-5 support and 1M token context windows.
### 🚀 [@ccusage/opencode](https://www.npmjs.com/package/@ccusage/opencode) - OpenCode Usage Analyzer
Companion tool for analyzing [OpenCode](https://github.com/opencode-ai/opencode) usage. Track token usage and costs from OpenCode sessions with the same reporting capabilities as ccusage.
### 🥧 [@ccusage/pi](https://www.npmjs.com/package/@ccusage/pi) - Pi-agent Usage Analyzer
Companion tool for analyzing [pi-agent](https://github.com/badlogic/pi-mono) session usage. Track token usage and costs from your pi-agent sessions with daily, monthly, and session-based reports.
### ⚡ [@ccusage/amp](https://www.npmjs.com/package/@ccusage/amp) - Amp Usage Analyzer
Companion tool for analyzing [Amp](https://ampcode.com/) session usage. Track token usage, costs, and credits from your Amp CLI sessions with daily, monthly, and session-based reports.
### 🔌 [@ccusage/mcp](https://www.npmjs.com/package/@ccusage/mcp) - MCP Server Integration
Model Context Protocol server that exposes ccusage data to Claude Desktop and other MCP-compatible tools. Enable real-time usage tracking directly in your AI workflows.
## Installation
### Quick Start (Recommended)
Thanks to ccusage's incredibly small bundle size ([](https://packagephobia.com/result?p=ccusage)), you can run it directly without installation:
```bash
# Recommended - always include @latest to ensure you get the newest version
npx ccusage@latest
bunx ccusage
# Alternative package runners
pnpm dlx ccusage
pnpx ccusage
# Using deno (with security flags)
deno run -E -R=$HOME/.claude/projects/ -S=homedir -N='raw.githubusercontent.com:443' npm:ccusage@latest
```
> 💡 **Important**: We strongly recommend using `@latest` suffix with npx (e.g., `npx ccusage@latest`) to ensure you're running the most recent version with the latest features and bug fixes.
### Related Tools
```bash
npx @ccusage/codex@latest # OpenAI Codex usage tracking
npx @ccusage/opencode@latest # OpenCode usage tracking
npx @ccusage/pi@latest # Pi-agent usage tracking
npx @ccusage/amp@latest # Amp usage tracking
npx @ccusage/mcp@latest # MCP Server
```
## Usage
```bash
# Basic usage
npx ccusage # Show daily report (default)
npx ccusage daily # Daily token usage and costs
npx ccusage monthly # Monthly aggregated report
npx ccusage session # Usage by conversation session
npx ccusage blocks # 5-hour billing windows
npx ccusage statusline # Compact status line for hooks (Beta)
# Filters and options
npx ccusage daily --since 20250525 --until 20250530
npx ccusage daily --json # JSON output
npx ccusage daily --breakdown # Per-model cost breakdown
npx ccusage daily --timezone UTC # Use UTC timezone
npx ccusage daily --locale ja-JP # Use Japanese locale for date/time formatting
# Project analysis
npx ccusage daily --instances # Group by project/instance
npx ccusage daily --project myproject # Filter to specific project
npx ccusage daily --instances --project myproject --json # Combined usage
# Compact mode for screenshots/sharing
npx ccusage --compact # Force compact table mode
npx ccusage monthly --compact # Compact monthly report
```
## Features
- 📊 **Daily Report**: View token usage and costs aggregated by date
- 📅 **Monthly Report**: View token usage and costs aggregated by month
- 💬 **Session Report**: View usage grouped by conversation sessions
- ⏰ **5-Hour Blocks Report**: Track usage within Claude's billing windows with active block monitoring
- 🚀 **Statusline Integration**: Compact usage display for Claude Code status bar hooks (Beta)
- 🤖 **Model Tracking**: See which Claude models you're using (Opus, Sonnet, etc.)
- 📊 **Model Breakdown**: View per-model cost breakdown with `--breakdown` flag
- 📅 **Date Filtering**: Filter reports by date range using `--since` and `--until`
- 📁 **Custom Path**: Support for custom Claude data directory locations
- 🎨 **Beautiful Output**: Colorful table-formatted display with automatic responsive layout
- 📱 **Smart Tables**: Automatic compact mode for narrow terminals (< 100 characters) with essential columns
- 📸 **Compact Mode**: Use `--compact` flag to force compact table layout, perfect for screenshots and sharing
- 📋 **Enhanced Model Display**: Model names shown as bulleted lists for better readability
- 📄 **JSON Output**: Export data in structured JSON format with `--json`
- 💰 **Cost Tracking**: Shows costs in USD for each day/month/session
- 🔄 **Cache Token Support**: Tracks and displays cache creation and cache read tokens separately
- 🌐 **Offline Mode**: Use pre-cached pricing data without network connectivity with `--offline` (Claude models only)
- 🔌 **MCP Integration**: Built-in Model Context Protocol server for integration with other tools
- 🏗️ **Multi-Instance Support**: Group usage by project with `--instances` flag and filter by specific projects
- 🌍 **Timezone Support**: Configure timezone for date grouping with `--timezone` option
- 🌐 **Locale Support**: Customize date/time formatting with `--locale` option (e.g., en-US, ja-JP, de-DE)
- ⚙️ **Configuration Files**: Set defaults with JSON configuration files, complete with IDE autocomplete and validation
- 🚀 **Ultra-Small Bundle**: Unlike other CLI tools, we pay extreme attention to bundle size - incredibly small even without minification!
## Documentation
Full documentation is available at **[ccusage.com](https://ccusage.com/)**
## Development Setup
### Using Nix (Recommended for Contributors)
For contributors and developers working on ccusage, we provide a Nix flake-based development environment:
```bash
# Clone the repository
git clone https://github.com/ryoppippi/ccusage.git
cd ccusage
# Allow direnv (automatically loads Nix environment)
direnv allow
# Or manually enter the development shell
nix develop
```
This ensures consistent tooling versions across all contributors and CI systems. The development environment is defined in `flake.nix` and automatically activated via direnv when entering the project directory.
## Sponsors
### Featured Sponsor
Check out [ccusage: The Claude Code cost scorecard that went viral](https://www.youtube.com/watch?v=Ak6qpQ5qdgk)
<p align="center">
<a href="https://www.youtube.com/watch?v=Ak6qpQ5qdgk">
<img src="https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/ccusage_thumbnail.png" alt="ccusage: The Claude Code cost scorecard that went viral" width="600">
</a>
</p>
<p align="center">
<a href="https://github.com/sponsors/ryoppippi">
<img src="https://cdn.jsdelivr.net/gh/ryoppippi/sponsors@main/sponsors.svg">
</a>
</p>
## Star History
<a href="https://www.star-history.com/#ryoppippi/ccusage&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=ryoppippi/ccusage&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=ryoppippi/ccusage&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=ryoppippi/ccusage&type=Date" />
</picture>
</a>
## License
[MIT](LICENSE) © [@ryoppippi](https://github.com/ryoppippi)
================================================
FILE: apps/ccusage/config-schema.json
================================================
{
"$ref": "#/definitions/ccusage-config",
"definitions": {
"ccusage-config": {
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema URL for validation and autocomplete",
"markdownDescription": "JSON Schema URL for validation and autocomplete"
},
"defaults": {
"type": "object",
"properties": {
"since": {
"type": "string",
"description": "Filter from date (YYYYMMDD format)",
"markdownDescription": "Filter from date (YYYYMMDD format)"
},
"until": {
"type": "string",
"description": "Filter until date (YYYYMMDD format)",
"markdownDescription": "Filter until date (YYYYMMDD format)"
},
"json": {
"type": "boolean",
"description": "Output in JSON format",
"markdownDescription": "Output in JSON format",
"default": false
},
"mode": {
"type": "string",
"enum": ["auto", "calculate", "display"],
"description": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"markdownDescription": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"default": "auto"
},
"debug": {
"type": "boolean",
"description": "Show pricing mismatch information for debugging",
"markdownDescription": "Show pricing mismatch information for debugging",
"default": false
},
"debugSamples": {
"type": "number",
"description": "Number of sample discrepancies to show in debug output (default: 5)",
"markdownDescription": "Number of sample discrepancies to show in debug output (default: 5)",
"default": 5
},
"order": {
"type": "string",
"enum": ["desc", "asc"],
"description": "Sort order: desc (newest first) or asc (oldest first)",
"markdownDescription": "Sort order: desc (newest first) or asc (oldest first)",
"default": "asc"
},
"breakdown": {
"type": "boolean",
"description": "Show per-model cost breakdown",
"markdownDescription": "Show per-model cost breakdown",
"default": false
},
"offline": {
"type": "boolean",
"description": "Use cached pricing data for Claude models instead of fetching from API",
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
"markdownDescription": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect."
},
"noColor": {
"type": "boolean",
"description": "Disable colored output (default: auto). NO_COLOR=1 has the same effect.",
"markdownDescription": "Disable colored output (default: auto). NO_COLOR=1 has the same effect."
},
"timezone": {
"type": "string",
"description": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone",
"markdownDescription": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone"
},
"locale": {
"type": "string",
"description": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"markdownDescription": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"default": "en-CA"
},
"jq": {
"type": "string",
"description": "Process JSON output with jq command (requires jq binary, implies --json)",
"markdownDescription": "Process JSON output with jq command (requires jq binary, implies --json)"
},
"compact": {
"type": "boolean",
"description": "Force compact mode for narrow displays (better for screenshots)",
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
}
},
"additionalProperties": false,
"description": "Default values for all commands",
"markdownDescription": "Default values for all commands"
},
"commands": {
"type": "object",
"properties": {
"daily": {
"type": "object",
"properties": {
"since": {
"type": "string",
"description": "Filter from date (YYYYMMDD format)",
"markdownDescription": "Filter from date (YYYYMMDD format)"
},
"until": {
"type": "string",
"description": "Filter until date (YYYYMMDD format)",
"markdownDescription": "Filter until date (YYYYMMDD format)"
},
"json": {
"type": "boolean",
"description": "Output in JSON format",
"markdownDescription": "Output in JSON format",
"default": false
},
"mode": {
"type": "string",
"enum": ["auto", "calculate", "display"],
"description": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"markdownDescription": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"default": "auto"
},
"debug": {
"type": "boolean",
"description": "Show pricing mismatch information for debugging",
"markdownDescription": "Show pricing mismatch information for debugging",
"default": false
},
"debugSamples": {
"type": "number",
"description": "Number of sample discrepancies to show in debug output (default: 5)",
"markdownDescription": "Number of sample discrepancies to show in debug output (default: 5)",
"default": 5
},
"order": {
"type": "string",
"enum": ["desc", "asc"],
"description": "Sort order: desc (newest first) or asc (oldest first)",
"markdownDescription": "Sort order: desc (newest first) or asc (oldest first)",
"default": "asc"
},
"breakdown": {
"type": "boolean",
"description": "Show per-model cost breakdown",
"markdownDescription": "Show per-model cost breakdown",
"default": false
},
"offline": {
"type": "boolean",
"description": "Use cached pricing data for Claude models instead of fetching from API",
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
"markdownDescription": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect."
},
"noColor": {
"type": "boolean",
"description": "Disable colored output (default: auto). NO_COLOR=1 has the same effect.",
"markdownDescription": "Disable colored output (default: auto). NO_COLOR=1 has the same effect."
},
"timezone": {
"type": "string",
"description": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone",
"markdownDescription": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone"
},
"locale": {
"type": "string",
"description": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"markdownDescription": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"default": "en-CA"
},
"jq": {
"type": "string",
"description": "Process JSON output with jq command (requires jq binary, implies --json)",
"markdownDescription": "Process JSON output with jq command (requires jq binary, implies --json)"
},
"compact": {
"type": "boolean",
"description": "Force compact mode for narrow displays (better for screenshots)",
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"instances": {
"type": "boolean",
"description": "Show usage breakdown by project/instance",
"markdownDescription": "Show usage breakdown by project/instance",
"default": false
},
"project": {
"type": "string",
"description": "Filter to specific project name",
"markdownDescription": "Filter to specific project name"
},
"projectAliases": {
"type": "string",
"description": "Comma-separated project aliases (e.g., 'ccusage=Usage Tracker,myproject=My Project')",
"markdownDescription": "Comma-separated project aliases (e.g., 'ccusage=Usage Tracker,myproject=My Project')"
}
},
"additionalProperties": false
},
"monthly": {
"type": "object",
"properties": {
"since": {
"type": "string",
"description": "Filter from date (YYYYMMDD format)",
"markdownDescription": "Filter from date (YYYYMMDD format)"
},
"until": {
"type": "string",
"description": "Filter until date (YYYYMMDD format)",
"markdownDescription": "Filter until date (YYYYMMDD format)"
},
"json": {
"type": "boolean",
"description": "Output in JSON format",
"markdownDescription": "Output in JSON format",
"default": false
},
"mode": {
"type": "string",
"enum": ["auto", "calculate", "display"],
"description": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"markdownDescription": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"default": "auto"
},
"debug": {
"type": "boolean",
"description": "Show pricing mismatch information for debugging",
"markdownDescription": "Show pricing mismatch information for debugging",
"default": false
},
"debugSamples": {
"type": "number",
"description": "Number of sample discrepancies to show in debug output (default: 5)",
"markdownDescription": "Number of sample discrepancies to show in debug output (default: 5)",
"default": 5
},
"order": {
"type": "string",
"enum": ["desc", "asc"],
"description": "Sort order: desc (newest first) or asc (oldest first)",
"markdownDescription": "Sort order: desc (newest first) or asc (oldest first)",
"default": "asc"
},
"breakdown": {
"type": "boolean",
"description": "Show per-model cost breakdown",
"markdownDescription": "Show per-model cost breakdown",
"default": false
},
"offline": {
"type": "boolean",
"description": "Use cached pricing data for Claude models instead of fetching from API",
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
"markdownDescription": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect."
},
"noColor": {
"type": "boolean",
"description": "Disable colored output (default: auto). NO_COLOR=1 has the same effect.",
"markdownDescription": "Disable colored output (default: auto). NO_COLOR=1 has the same effect."
},
"timezone": {
"type": "string",
"description": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone",
"markdownDescription": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone"
},
"locale": {
"type": "string",
"description": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"markdownDescription": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"default": "en-CA"
},
"jq": {
"type": "string",
"description": "Process JSON output with jq command (requires jq binary, implies --json)",
"markdownDescription": "Process JSON output with jq command (requires jq binary, implies --json)"
},
"compact": {
"type": "boolean",
"description": "Force compact mode for narrow displays (better for screenshots)",
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
}
},
"additionalProperties": false
},
"weekly": {
"type": "object",
"properties": {
"since": {
"type": "string",
"description": "Filter from date (YYYYMMDD format)",
"markdownDescription": "Filter from date (YYYYMMDD format)"
},
"until": {
"type": "string",
"description": "Filter until date (YYYYMMDD format)",
"markdownDescription": "Filter until date (YYYYMMDD format)"
},
"json": {
"type": "boolean",
"description": "Output in JSON format",
"markdownDescription": "Output in JSON format",
"default": false
},
"mode": {
"type": "string",
"enum": ["auto", "calculate", "display"],
"description": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"markdownDescription": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"default": "auto"
},
"debug": {
"type": "boolean",
"description": "Show pricing mismatch information for debugging",
"markdownDescription": "Show pricing mismatch information for debugging",
"default": false
},
"debugSamples": {
"type": "number",
"description": "Number of sample discrepancies to show in debug output (default: 5)",
"markdownDescription": "Number of sample discrepancies to show in debug output (default: 5)",
"default": 5
},
"order": {
"type": "string",
"enum": ["desc", "asc"],
"description": "Sort order: desc (newest first) or asc (oldest first)",
"markdownDescription": "Sort order: desc (newest first) or asc (oldest first)",
"default": "asc"
},
"breakdown": {
"type": "boolean",
"description": "Show per-model cost breakdown",
"markdownDescription": "Show per-model cost breakdown",
"default": false
},
"offline": {
"type": "boolean",
"description": "Use cached pricing data for Claude models instead of fetching from API",
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
"markdownDescription": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect."
},
"noColor": {
"type": "boolean",
"description": "Disable colored output (default: auto). NO_COLOR=1 has the same effect.",
"markdownDescription": "Disable colored output (default: auto). NO_COLOR=1 has the same effect."
},
"timezone": {
"type": "string",
"description": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone",
"markdownDescription": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone"
},
"locale": {
"type": "string",
"description": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"markdownDescription": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"default": "en-CA"
},
"jq": {
"type": "string",
"description": "Process JSON output with jq command (requires jq binary, implies --json)",
"markdownDescription": "Process JSON output with jq command (requires jq binary, implies --json)"
},
"compact": {
"type": "boolean",
"description": "Force compact mode for narrow displays (better for screenshots)",
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"startOfWeek": {
"type": "string",
"enum": [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday"
],
"description": "Day to start the week on",
"markdownDescription": "Day to start the week on",
"default": "sunday"
}
},
"additionalProperties": false
},
"session": {
"type": "object",
"properties": {
"since": {
"type": "string",
"description": "Filter from date (YYYYMMDD format)",
"markdownDescription": "Filter from date (YYYYMMDD format)"
},
"until": {
"type": "string",
"description": "Filter until date (YYYYMMDD format)",
"markdownDescription": "Filter until date (YYYYMMDD format)"
},
"json": {
"type": "boolean",
"description": "Output in JSON format",
"markdownDescription": "Output in JSON format",
"default": false
},
"mode": {
"type": "string",
"enum": ["auto", "calculate", "display"],
"description": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"markdownDescription": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"default": "auto"
},
"debug": {
"type": "boolean",
"description": "Show pricing mismatch information for debugging",
"markdownDescription": "Show pricing mismatch information for debugging",
"default": false
},
"debugSamples": {
"type": "number",
"description": "Number of sample discrepancies to show in debug output (default: 5)",
"markdownDescription": "Number of sample discrepancies to show in debug output (default: 5)",
"default": 5
},
"breakdown": {
"type": "boolean",
"description": "Show per-model cost breakdown",
"markdownDescription": "Show per-model cost breakdown",
"default": false
},
"offline": {
"type": "boolean",
"description": "Use cached pricing data for Claude models instead of fetching from API",
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
"markdownDescription": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect."
},
"noColor": {
"type": "boolean",
"description": "Disable colored output (default: auto). NO_COLOR=1 has the same effect.",
"markdownDescription": "Disable colored output (default: auto). NO_COLOR=1 has the same effect."
},
"timezone": {
"type": "string",
"description": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone",
"markdownDescription": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone"
},
"locale": {
"type": "string",
"description": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"markdownDescription": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"default": "en-CA"
},
"jq": {
"type": "string",
"description": "Process JSON output with jq command (requires jq binary, implies --json)",
"markdownDescription": "Process JSON output with jq command (requires jq binary, implies --json)"
},
"compact": {
"type": "boolean",
"description": "Force compact mode for narrow displays (better for screenshots)",
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"id": {
"type": "string",
"description": "Load usage data for a specific session ID",
"markdownDescription": "Load usage data for a specific session ID"
}
},
"additionalProperties": false
},
"blocks": {
"type": "object",
"properties": {
"since": {
"type": "string",
"description": "Filter from date (YYYYMMDD format)",
"markdownDescription": "Filter from date (YYYYMMDD format)"
},
"until": {
"type": "string",
"description": "Filter until date (YYYYMMDD format)",
"markdownDescription": "Filter until date (YYYYMMDD format)"
},
"json": {
"type": "boolean",
"description": "Output in JSON format",
"markdownDescription": "Output in JSON format",
"default": false
},
"mode": {
"type": "string",
"enum": ["auto", "calculate", "display"],
"description": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"markdownDescription": "Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)",
"default": "auto"
},
"debug": {
"type": "boolean",
"description": "Show pricing mismatch information for debugging",
"markdownDescription": "Show pricing mismatch information for debugging",
"default": false
},
"debugSamples": {
"type": "number",
"description": "Number of sample discrepancies to show in debug output (default: 5)",
"markdownDescription": "Number of sample discrepancies to show in debug output (default: 5)",
"default": 5
},
"order": {
"type": "string",
"enum": ["desc", "asc"],
"description": "Sort order: desc (newest first) or asc (oldest first)",
"markdownDescription": "Sort order: desc (newest first) or asc (oldest first)",
"default": "asc"
},
"breakdown": {
"type": "boolean",
"description": "Show per-model cost breakdown",
"markdownDescription": "Show per-model cost breakdown",
"default": false
},
"offline": {
"type": "boolean",
"description": "Use cached pricing data for Claude models instead of fetching from API",
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
"markdownDescription": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect."
},
"noColor": {
"type": "boolean",
"description": "Disable colored output (default: auto). NO_COLOR=1 has the same effect.",
"markdownDescription": "Disable colored output (default: auto). NO_COLOR=1 has the same effect."
},
"timezone": {
"type": "string",
"description": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone",
"markdownDescription": "Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone"
},
"locale": {
"type": "string",
"description": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"markdownDescription": "Locale for date/time formatting (e.g., en-US, ja-JP, de-DE)",
"default": "en-CA"
},
"jq": {
"type": "string",
"description": "Process JSON output with jq command (requires jq binary, implies --json)",
"markdownDescription": "Process JSON output with jq command (requires jq binary, implies --json)"
},
"compact": {
"type": "boolean",
"description": "Force compact mode for narrow displays (better for screenshots)",
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"active": {
"type": "boolean",
"description": "Show only active block with projections",
"markdownDescription": "Show only active block with projections",
"default": false
},
"recent": {
"type": "boolean",
"description": "Show blocks from last 3 days (including active)",
"markdownDescription": "Show blocks from last 3 days (including active)",
"default": false
},
"tokenLimit": {
"type": "string",
"description": "Token limit for quota warnings (e.g., 500000 or \"max\")",
"markdownDescription": "Token limit for quota warnings (e.g., 500000 or \"max\")"
},
"sessionLength": {
"type": "number",
"description": "Session block duration in hours (default: 5)",
"markdownDescription": "Session block duration in hours (default: 5)",
"default": 5
}
},
"additionalProperties": false
},
"statusline": {
"type": "object",
"properties": {
"offline": {
"type": "boolean",
"description": "Use cached pricing data for Claude models instead of fetching from API",
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": true
},
"visualBurnRate": {
"type": "string",
"enum": ["off", "emoji", "text", "emoji-text"],
"description": "Controls the visualization of the burn rate status",
"markdownDescription": "Controls the visualization of the burn rate status",
"default": "off"
},
"costSource": {
"type": "string",
"enum": ["auto", "ccusage", "cc", "both"],
"description": "Session cost source: auto (prefer CC then ccusage), ccusage (always calculate), cc (always use Claude Code cost), both (show both costs)",
"markdownDescription": "Session cost source: auto (prefer CC then ccusage), ccusage (always calculate), cc (always use Claude Code cost), both (show both costs)",
"default": "auto"
},
"cache": {
"type": "boolean",
"description": "Enable cache for status line output (default: true)",
"markdownDescription": "Enable cache for status line output (default: true)",
"default": true
},
"refreshInterval": {
"type": "number",
"description": "Refresh interval in seconds for cache expiry (default: 1)",
"markdownDescription": "Refresh interval in seconds for cache expiry (default: 1)",
"default": 1
},
"contextLowThreshold": {
"type": "string",
"description": "Context usage percentage below which status is shown in green (0-100)",
"markdownDescription": "Context usage percentage below which status is shown in green (0-100)",
"default": 50
},
"contextMediumThreshold": {
"type": "string",
"description": "Context usage percentage below which status is shown in yellow (0-100)",
"markdownDescription": "Context usage percentage below which status is shown in yellow (0-100)",
"default": 80
},
"debug": {
"type": "boolean",
"description": "Show pricing mismatch information for debugging",
"markdownDescription": "Show pricing mismatch information for debugging",
"default": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"description": "Command-specific configuration overrides",
"markdownDescription": "Command-specific configuration overrides"
}
},
"additionalProperties": false
}
},
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "ccusage Configuration",
"description": "Configuration file for ccusage - Claude Code usage analysis tool",
"examples": [
{
"$schema": "https://ccusage.com/config-schema.json",
"defaults": {
"json": false,
"mode": "auto",
"timezone": "Asia/Tokyo",
"locale": "ja-JP"
},
"commands": {
"daily": {
"instances": true
},
"blocks": {
"tokenLimit": "500000"
}
}
}
]
}
================================================
FILE: apps/ccusage/eslint.config.js
================================================
import { ryoppippi } from '@ryoppippi/eslint-config';
/** @type {import('eslint').Linter.FlatConfig[]} */
const config = ryoppippi(
{
type: 'lib',
stylistic: false,
},
{
rules: {
'test/no-importing-vitest-globals': 'error',
},
},
);
export default config;
================================================
FILE: apps/ccusage/package.json
================================================
{
"name": "ccusage",
"type": "module",
"version": "18.0.10",
"description": "Usage analysis tool for Claude Code",
"author": "ryoppippi",
"license": "MIT",
"funding": "https://github.com/ryoppippi/ccusage?sponsor=1",
"homepage": "https://github.com/ryoppippi/ccusage#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ryoppippi/ccusage.git",
"directory": "apps/ccusage"
},
"bugs": {
"url": "https://github.com/ryoppippi/ccusage/issues"
},
"exports": {
".": "./src/index.ts",
"./calculate-cost": "./src/calculate-cost.ts",
"./data-loader": "./src/data-loader.ts",
"./debug": "./src/debug.ts",
"./logger": "./src/logger.ts",
"./package.json": "./package.json"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"ccusage": "./src/index.ts"
},
"files": [
"config-schema.json",
"dist"
],
"publishConfig": {
"bin": {
"ccusage": "./dist/index.js"
},
"exports": {
".": "./dist/index.js",
"./calculate-cost": "./dist/calculate-cost.js",
"./data-loader": "./dist/data-loader.js",
"./debug": "./dist/debug.js",
"./logger": "./dist/logger.js",
"./package.json": "./package.json"
}
},
"engines": {
"node": ">=20.19.4"
},
"scripts": {
"build": "pnpm run generate:schema && tsdown",
"format": "pnpm run lint --fix",
"generate:schema": "bun scripts/generate-json-schema.ts",
"lint": "eslint --cache .",
"prepack": "pnpm run build && clean-pkg-json",
"prepare": "pnpm run generate:schema || true",
"prerelease": "pnpm run lint && pnpm run typecheck && pnpm run build",
"start": "bun ./src/index.ts",
"test": "TZ=UTC vitest",
"test:statusline": "cat test/statusline-test.json | node ./src/index.ts statusline",
"test:statusline:all": "echo 'Testing Sonnet 4:' && pnpm run test:statusline:sonnet4 && echo 'Testing Opus 4.1:' && pnpm run test:statusline:opus4 && echo 'Testing Sonnet 4.1:' && pnpm run test:statusline:sonnet41",
"test:statusline:opus4": "cat test/statusline-test-opus4.json | node ./src/index.ts statusline --offline",
"test:statusline:sonnet4": "cat test/statusline-test-sonnet4.json | node ./src/index.ts statusline --offline",
"test:statusline:sonnet41": "cat test/statusline-test-sonnet41.json | node ./src/index.ts statusline --offline",
"typecheck": "tsgo --noEmit"
},
"devDependencies": {
"@antfu/utils": "catalog:runtime",
"@ccusage/internal": "workspace:*",
"@ccusage/terminal": "workspace:*",
"@oxc-project/runtime": "catalog:build",
"@praha/byethrow": "catalog:runtime",
"@ryoppippi/eslint-config": "catalog:lint",
"@ryoppippi/limo": "catalog:runtime",
"@std/async": "catalog:runtime",
"@types/bun": "catalog:types",
"@typescript/native-preview": "catalog:types",
"ansi-escapes": "catalog:runtime",
"bumpp": "catalog:release",
"clean-pkg-json": "catalog:release",
"es-toolkit": "catalog:runtime",
"eslint": "catalog:lint",
"eslint-plugin-format": "catalog:lint",
"fast-sort": "catalog:runtime",
"fs-fixture": "catalog:testing",
"get-stdin": "catalog:runtime",
"gunshi": "catalog:runtime",
"nano-spawn": "catalog:runtime",
"p-limit": "catalog:runtime",
"path-type": "catalog:runtime",
"picocolors": "catalog:runtime",
"pretty-ms": "catalog:runtime",
"publint": "catalog:lint",
"string-width": "catalog:runtime",
"tinyglobby": "catalog:runtime",
"tsdown": "catalog:build",
"type-fest": "catalog:runtime",
"unplugin-macros": "catalog:build",
"unplugin-unused": "catalog:build",
"valibot": "catalog:runtime",
"vitest": "catalog:testing",
"xdg-basedir": "catalog:runtime"
}
}
================================================
FILE: apps/ccusage/scripts/generate-json-schema.ts
================================================
#!/usr/bin/env bun
/**
* @fileoverview Generate JSON Schema from args-tokens configuration schema
*
* This script generates a JSON Schema file from the args-tokens configuration schema
* for ccusage configuration files. The generated schema enables:
* - IDE autocomplete and validation
* - Documentation of available options
* - Schema validation for configuration files
*/
import process from 'node:process';
import { Result } from '@praha/byethrow';
import { $ } from 'bun';
import { sharedArgs } from '../src/_shared-args.ts';
// Import command definitions to access their args
import { subCommandUnion } from '../src/commands/index.ts';
import { logger } from '../src/logger.ts';
/**
* The filename for the generated JSON Schema file.
* Used for both root directory and docs/public directory output.
*/
const SCHEMA_FILENAME = 'config-schema.json';
/**
* Keys to exclude from the generated JSON Schema.
* These are CLI-only options that shouldn't appear in configuration files.
*/
const EXCLUDE_KEYS = ['config'];
/**
* Command-specific keys to exclude from the generated JSON Schema.
* These are CLI-only options that shouldn't appear in configuration files.
*/
const COMMAND_EXCLUDE_KEYS: Record<string, string[]> = {
blocks: ['live', 'refreshInterval'],
};
/**
* Convert args-tokens schema to JSON Schema format
*/
function tokensSchemaToJsonSchema(schema: Record<string, any>): Record<string, any> {
const properties: Record<string, any> = {};
for (const [key, arg] of Object.entries(schema)) {
// eslint-disable-next-line ts/no-unsafe-assignment
const argTyped = arg;
const property: Record<string, any> = {};
// Handle type conversion
// eslint-disable-next-line ts/no-unsafe-member-access
switch (argTyped.type) {
case 'boolean':
property.type = 'boolean';
break;
case 'number':
property.type = 'number';
break;
case 'string':
case 'custom':
property.type = 'string';
break;
case 'enum':
property.type = 'string';
// eslint-disable-next-line ts/no-unsafe-member-access
if (argTyped.choices != null && Array.isArray(argTyped.choices)) {
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
property.enum = argTyped.choices;
}
break;
default:
property.type = 'string';
}
// Add description
// eslint-disable-next-line ts/no-unsafe-member-access
if (argTyped.description != null) {
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
property.description = argTyped.description;
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
property.markdownDescription = argTyped.description;
}
// Add default value
// eslint-disable-next-line ts/no-unsafe-member-access
if ('default' in argTyped && argTyped.default !== undefined) {
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
property.default = argTyped.default;
}
properties[key] = property;
}
return {
type: 'object',
properties,
additionalProperties: false,
};
}
/**
* Create the complete configuration schema from all command definitions
*/
function createConfigSchemaJson() {
// Create schema for default/shared arguments (excluding CLI-only options)
const defaultsSchema = Object.fromEntries(
Object.entries(sharedArgs).filter(([key]) => !EXCLUDE_KEYS.includes(key)),
);
// Create schemas for each command's specific arguments (excluding CLI-only options)
const commandSchemas: Record<string, any> = {};
for (const [commandName, command] of subCommandUnion) {
const commandExcludes = COMMAND_EXCLUDE_KEYS[commandName] ?? [];
commandSchemas[commandName] = Object.fromEntries(
Object.entries(command.args as Record<string, any>).filter(
([key]) => !EXCLUDE_KEYS.includes(key) && !commandExcludes.includes(key),
),
);
}
// Convert to JSON Schema format
const defaultsJsonSchema = tokensSchemaToJsonSchema(defaultsSchema);
const commandsJsonSchema = {
type: 'object',
properties: Object.fromEntries(
Object.entries(commandSchemas).map(([name, schema]) => [
name,
// eslint-disable-next-line ts/no-unsafe-argument
tokensSchemaToJsonSchema(schema),
]),
),
additionalProperties: false,
description: 'Command-specific configuration overrides',
markdownDescription: 'Command-specific configuration overrides',
};
// Main configuration schema
return {
$ref: '#/definitions/ccusage-config',
definitions: {
'ccusage-config': {
type: 'object',
properties: {
$schema: {
type: 'string',
description: 'JSON Schema URL for validation and autocomplete',
markdownDescription: 'JSON Schema URL for validation and autocomplete',
},
defaults: {
...defaultsJsonSchema,
description: 'Default values for all commands',
markdownDescription: 'Default values for all commands',
},
commands: commandsJsonSchema,
},
additionalProperties: false,
},
},
$schema: 'https://json-schema.org/draft-07/schema#',
title: 'ccusage Configuration',
description: 'Configuration file for ccusage - Claude Code usage analysis tool',
examples: [
{
$schema: 'https://ccusage.com/config-schema.json',
defaults: {
json: false,
mode: 'auto',
timezone: 'Asia/Tokyo',
locale: 'ja-JP',
},
commands: {
daily: {
instances: true,
},
blocks: {
tokenLimit: '500000',
},
},
},
],
};
}
/**
* Generate JSON Schema and write to files
*/
async function runFormat(files: string[]) {
return Result.try({
try: $`pnpm exec oxfmt ${files}`,
catch: (error) => error,
});
}
async function writeFile(path: string, content: string) {
const attempt = Result.try({
try: async () => Bun.write(path, content),
catch: (error) => error,
});
return attempt();
}
async function readFile(path: string): Promise<Result.Result<string, any>> {
return Result.try({
try: async () => {
const file = Bun.file(path);
return file.text();
},
catch: (error) => error,
})();
}
async function copySchemaToDocsPublic() {
const gitRoot = await $`git rev-parse --show-toplevel`.text().then((text) => text.trim());
await $`cp ${SCHEMA_FILENAME} ${gitRoot}/docs/public/${SCHEMA_FILENAME}`;
}
async function generateJsonSchema() {
logger.info('Generating JSON Schema from args-tokens configuration schema...');
// Create the JSON Schema
const schemaObject = Result.pipe(
Result.try({
try: () => createConfigSchemaJson(),
catch: (error) => error,
})(),
Result.inspectError((error) => {
logger.error('Error creating JSON Schema:', error);
process.exit(1);
}),
Result.unwrap(),
);
// Check if existing root schema is identical to avoid unnecessary writes
const existingRootSchema = await Result.pipe(
readFile(SCHEMA_FILENAME),
Result.map((content) => JSON.parse(content) as unknown),
Result.unwrap(''),
);
const isSchemaChanged = !Bun.deepEquals(existingRootSchema, schemaObject, true);
if (!isSchemaChanged) {
logger.info('✓ Root schema is up to date, skipping generation');
// Always copy to docs/public since it's gitignored
await copySchemaToDocsPublic();
logger.info('JSON Schema sync completed successfully!');
return;
}
const schemaJson = JSON.stringify(schemaObject, null, '\t');
await Result.pipe(
Result.try({
try: writeFile(SCHEMA_FILENAME, schemaJson),
safe: true,
}),
Result.inspectError((error) => {
logger.error(`Failed to write ${SCHEMA_FILENAME}:`, error);
process.exit(1);
}),
Result.inspect(() => logger.info(`✓ Generated ${SCHEMA_FILENAME}`)),
);
// Copy to docs/public using Bun shell
await copySchemaToDocsPublic();
// Run format on the root schema file that was changed
await Result.pipe(
Result.try({
try: runFormat([SCHEMA_FILENAME]),
safe: true,
}),
Result.inspectError((error) => {
logger.error('Failed to format generated files:', error);
process.exit(1);
}),
Result.inspect(() => logger.info('✓ Formatted generated files')),
);
logger.info('JSON Schema generation completed successfully!');
}
// Run the generator
if (import.meta.main) {
await generateJsonSchema();
}
if (import.meta.vitest != null) {
describe('tokensSchemaToJsonSchema', () => {
it('should convert boolean args to JSON Schema', () => {
const schema = {
debug: {
type: 'boolean',
description: 'Enable debug mode',
default: false,
},
};
const jsonSchema = tokensSchemaToJsonSchema(schema);
expect((jsonSchema.properties as Record<string, any>).debug).toEqual({
type: 'boolean',
description: 'Enable debug mode',
markdownDescription: 'Enable debug mode',
default: false,
});
});
it('should convert enum args to JSON Schema', () => {
const schema = {
mode: {
type: 'enum',
description: 'Mode selection',
choices: ['auto', 'manual'],
default: 'auto',
},
};
const jsonSchema = tokensSchemaToJsonSchema(schema);
expect((jsonSchema.properties as Record<string, any>).mode).toEqual({
type: 'string',
enum: ['auto', 'manual'],
description: 'Mode selection',
markdownDescription: 'Mode selection',
default: 'auto',
});
});
});
describe('createConfigSchemaJson', () => {
it('should generate valid JSON Schema', () => {
const jsonSchema = createConfigSchemaJson();
expect(jsonSchema).toBeDefined();
expect(jsonSchema.$ref).toBe('#/definitions/ccusage-config');
expect(jsonSchema.definitions).toBeDefined();
expect(jsonSchema.definitions['ccusage-config']).toBeDefined();
expect(jsonSchema.definitions['ccusage-config'].type).toBe('object');
});
it('should include all expected properties', () => {
const jsonSchema = createConfigSchemaJson();
const mainSchema = jsonSchema.definitions['ccusage-config'];
expect(mainSchema.properties).toHaveProperty('$schema');
expect(mainSchema.properties).toHaveProperty('defaults');
expect(mainSchema.properties).toHaveProperty('commands');
});
it('should include all command schemas', () => {
const jsonSchema = createConfigSchemaJson();
const commandsSchema = jsonSchema.definitions['ccusage-config'].properties.commands;
expect(commandsSchema.properties).toHaveProperty('daily');
expect(commandsSchema.properties).toHaveProperty('monthly');
expect(commandsSchema.properties).toHaveProperty('weekly');
expect(commandsSchema.properties).toHaveProperty('session');
expect(commandsSchema.properties).toHaveProperty('blocks');
expect(commandsSchema.properties).toHaveProperty('mcp');
expect(commandsSchema.properties).toHaveProperty('statusline');
});
});
}
================================================
FILE: apps/ccusage/src/_config-loader-tokens.ts
================================================
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import process from 'node:process';
import { toArray } from '@antfu/utils';
import { Result } from '@praha/byethrow';
import { createFixture } from 'fs-fixture';
import { CONFIG_FILE_NAME } from './_consts.ts';
import { getClaudePaths } from './data-loader.ts';
import { logger } from './logger.ts';
/**
* Minimal command context interface for config merging
* Contains only the properties we need from Gunshi's CommandContext
*/
export type ConfigMergeContext<T extends Record<string, unknown>> = {
/** Command values from CLI */
values: T;
/** Command tokens from CLI */
tokens: unknown[];
/** Command name being executed */
name?: string;
};
/**
* Extract explicitly provided arguments from gunshi tokens
* @param tokens - Command tokens from ctx.tokens
* @returns Object with keys as argument names and values as boolean (true if explicitly provided)
*/
function extractExplicitArgs(tokens: unknown[]): Record<string, boolean> {
const explicit: Record<string, boolean> = {};
for (const token of tokens) {
if (typeof token === 'object' && token !== null) {
const t = token as { kind?: string; name?: string };
if (t.kind === 'option' && typeof t.name === 'string') {
explicit[t.name] = true;
}
}
}
return explicit;
}
// Type for configuration data (simple structure without Valibot)
export type ConfigData = {
$schema?: string;
defaults?: Record<string, any>;
commands?: Record<string, Record<string, any>>;
source?: string;
};
/**
* Get configuration file search paths in priority order (highest to lowest)
* 1. Local .ccusage/ccusage.json
* 2. User config directories from getClaudePaths() + ccusage.json
*/
function getConfigSearchPaths(): string[] {
const claudeConfigDirs = [join(process.cwd(), '.ccusage'), ...toArray(getClaudePaths())];
return claudeConfigDirs.map((dir) => join(dir, CONFIG_FILE_NAME));
}
/**
* Basic JSON validation - just check if it can be parsed and has expected structure
*/
function validateConfigJson(data: unknown): data is ConfigData {
if (typeof data !== 'object' || data === null) {
return false;
}
const config = data as Record<string, unknown>;
// Optional schema property
if (config.$schema != null && typeof config.$schema !== 'string') {
return false;
}
// Optional defaults property
if (
config.defaults != null &&
(typeof config.defaults !== 'object' || config.defaults === null)
) {
return false;
}
// Optional commands property
if (
config.commands != null &&
(typeof config.commands !== 'object' || config.commands === null)
) {
return false;
}
return true;
}
/**
* Internal function to load and parse a configuration file
* @param filePath - Path to the configuration file
* @param debug - Whether to enable debug logging
* @returns ConfigData if successful, undefined if failed
*/
function loadConfigFile(filePath: string, debug = false): ConfigData | undefined {
if (!existsSync(filePath)) {
if (debug) {
logger.info(` • Checking: ${filePath} (not found)`);
}
return undefined;
}
const parseConfigFileResult = Result.pipe(
Result.try({
try: () => {
const content = readFileSync(filePath, 'utf-8');
const data = JSON.parse(content) as unknown;
if (!validateConfigJson(data)) {
throw new Error('Invalid configuration structure');
}
// Add source path to the config for debug display
data.source = filePath;
return data;
},
catch: (error) => error,
})(),
Result.inspect(() => {
logger.debug(`Parsed configuration file: ${filePath}`);
if (debug) {
logger.info(` • Checking: ${filePath} (found ✓)`);
}
}),
Result.inspectError((error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.warn(`Error parsing configuration file at ${filePath}: ${errorMessage}`);
if (debug) {
logger.info(` • Checking: ${filePath} (error: ${errorMessage})`);
}
}),
Result.unwrap(undefined),
);
return parseConfigFileResult;
}
/**
* Loads configuration from the specified path or auto-discovery
* @param configPath - Optional path to specific config file
* @param debug - Whether to enable debug logging
* @returns Parsed configuration data or undefined if no config found
*/
export function loadConfig(configPath?: string, debug = false): ConfigData | undefined {
if (debug) {
logger.info('Debug mode enabled - showing config loading details\n');
}
// If specific config path is provided, use it exclusively
if (configPath != null) {
if (debug) {
logger.info('Using specified config file:');
logger.info(` • Path: ${configPath}`);
}
const config = loadConfigFile(configPath, debug);
if (config == null) {
logger.warn(`Configuration file not found or invalid: ${configPath}`);
} else if (debug) {
logger.info('');
logger.info(`Loaded config from: ${configPath}`);
logger.info(` • Schema: ${config.$schema ?? 'none'}`);
logger.info(
` • Has defaults: ${config.defaults != null ? 'yes' : 'no'}${config.defaults != null ? ` (${Object.keys(config.defaults).length} options)` : ''}`,
);
logger.info(
` • Has command configs: ${config.commands != null ? 'yes' : 'no'}${config.commands != null ? ` (${Object.keys(config.commands).join(', ')})` : ''}`,
);
}
return config;
}
// Auto-discovery from search paths (highest priority first)
if (debug) {
logger.info('Searching for config files:');
}
for (const searchPath of getConfigSearchPaths()) {
const config = loadConfigFile(searchPath, debug);
if (config != null) {
if (debug) {
logger.info('');
logger.info(`Loaded config from: ${searchPath}`);
logger.info(` • Schema: ${config.$schema ?? 'none'}`);
logger.info(
` • Has defaults: ${config.defaults != null ? 'yes' : 'no'}${config.defaults != null ? ` (${Object.keys(config.defaults).length} options)` : ''}`,
);
logger.info(
` • Has command configs: ${config.commands != null ? 'yes' : 'no'}${config.commands != null ? ` (${Object.keys(config.commands).join(', ')})` : ''}`,
);
}
return config;
}
// Continue searching other paths even if one config is invalid
}
logger.debug('No valid configuration file found');
if (debug) {
logger.info('');
logger.info('No valid configuration file found');
}
return undefined;
}
/**
* Merges configuration with CLI arguments
* Priority order (highest to lowest):
* 1. CLI arguments (ctx.values)
* 2. Command-specific config
* 3. Default config
* 4. Gunshi defaults
*
* @param ctx - Command context with values, tokens, and name
* @param config - Loaded configuration data
* @param debug - Whether to enable debug logging
* @returns Merged arguments object
*/
export function mergeConfigWithArgs<T extends Record<string, unknown>>(
ctx: ConfigMergeContext<T>,
config?: ConfigData,
debug = false,
): T {
if (config == null) {
if (debug) {
logger.info('');
logger.info(
`No config file loaded, using CLI args only for '${ctx.name ?? 'unknown'}' command`,
);
}
return ctx.values;
}
// Start with an empty base
const merged = {} as T;
const commandName = ctx.name;
// Track sources for debug output
const sources: Record<string, string> = {};
// 1. Apply defaults from config (lowest priority)
if (config.defaults != null) {
for (const [key, value] of Object.entries(config.defaults)) {
(merged as Record<string, unknown>)[key] = value;
sources[key] = 'defaults';
}
}
// 2. Apply command-specific config
if (commandName != null && config.commands?.[commandName] != null) {
for (const [key, value] of Object.entries(config.commands[commandName])) {
(merged as Record<string, unknown>)[key] = value;
sources[key] = 'command config';
}
}
// 3. Apply CLI arguments (highest priority)
// Only override with CLI args that are explicitly provided by the user
const explicit = extractExplicitArgs(ctx.tokens);
for (const [key, value] of Object.entries(ctx.values)) {
if (value != null && explicit[key] === true) {
// eslint-disable-next-line ts/no-unsafe-member-access
(merged as any)[key] = value;
sources[key] = 'CLI';
}
}
logger.debug(`Merged config for ${commandName ?? 'unknown'}:`, merged);
if (debug) {
logger.info('');
logger.info(`Merging options for '${commandName ?? 'unknown'}' command:`);
// Group options by source
const bySource: Record<string, string[]> = {
defaults: [],
'command config': [],
CLI: [],
};
for (const [key, source] of Object.entries(sources)) {
if (bySource[source] != null) {
bySource[source].push(`${key}=${JSON.stringify((merged as Record<string, unknown>)[key])}`);
}
}
if (bySource.defaults!.length > 0) {
logger.info(` • From defaults: ${bySource.defaults!.join(', ')}`);
}
if (bySource['command config']!.length > 0) {
logger.info(` • From command config: ${bySource['command config']!.join(', ')}`);
}
if (bySource.CLI!.length > 0) {
logger.info(` • From CLI args: ${bySource.CLI!.join(', ')}`);
}
// Show final result with sources
logger.info(' • Final merged options: {');
for (const [key, value] of Object.entries(merged)) {
const source = sources[key] ?? 'unknown';
logger.info(` ${key}: ${JSON.stringify(value)} (from ${source}),`);
}
logger.info(' }');
}
return merged;
}
/**
* Validates a configuration file without loading it
* @param configPath - Path to configuration file
* @returns Validation result
*/
export function validateConfigFile(
configPath: string,
): { success: true; data: ConfigData } | { success: false; error: Error } {
if (!existsSync(configPath)) {
return { success: false, error: new Error(`Configuration file does not exist: ${configPath}`) };
}
const parseConfig = Result.try({
try: () => {
const content = readFileSync(configPath, 'utf-8');
const data = JSON.parse(content) as unknown;
if (!validateConfigJson(data)) {
throw new Error('Invalid configuration structure');
}
return data;
},
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
});
const result = parseConfig();
if (Result.isSuccess(result)) {
return { success: true, data: result.value };
} else {
return { success: false, error: result.error };
}
}
if (import.meta.vitest != null) {
describe('extractExplicitArgs', () => {
it('should extract explicit arguments from tokens', () => {
const tokens = [
{ kind: 'option', name: 'json' },
{ kind: 'option', name: 'debug' },
{ kind: 'positional', value: 'daily' }, // Should be ignored
{ kind: 'option', name: 'mode' },
];
const result = extractExplicitArgs(tokens);
expect(result).toEqual({
json: true,
debug: true,
mode: true,
});
});
it('should handle empty tokens array', () => {
const result = extractExplicitArgs([]);
expect(result).toEqual({});
});
it('should handle invalid token structures', () => {
const tokens = [
null,
undefined,
'string',
123,
{ kind: 'option' }, // Missing name
{ name: 'test' }, // Missing kind
{ kind: 'other', name: 'ignored' }, // Wrong kind
];
const result = extractExplicitArgs(tokens);
expect(result).toEqual({});
});
it('should handle mixed valid and invalid tokens', () => {
const tokens = [
{ kind: 'option', name: 'valid' },
null,
{ kind: 'positional', value: 'ignored' },
{ kind: 'option', name: 'alsoValid' },
];
const result = extractExplicitArgs(tokens);
expect(result).toEqual({
valid: true,
alsoValid: true,
});
});
});
describe('loadConfig', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should load valid configuration from .ccusage/ccusage.json', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': JSON.stringify({
defaults: { json: true },
commands: { daily: { instances: true } },
}),
});
// Mock process.cwd to return fixture path
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
const config = loadConfig();
expect(config).toBeDefined();
expect(config?.defaults?.json).toBe(true);
expect(config?.commands?.daily?.instances).toBe(true);
});
it('should load configuration with specific path', async () => {
await using fixture = await createFixture({
'custom-config.json': JSON.stringify({
defaults: { debug: true },
commands: { monthly: { breakdown: true } },
}),
});
const config = loadConfig(fixture.getPath('custom-config.json'));
expect(config).toBeDefined();
expect(config?.defaults?.debug).toBe(true);
expect(config?.commands?.monthly?.breakdown).toBe(true);
});
it('should return undefined for non-existent config file', () => {
const config = loadConfig('/non/existent/path.json');
expect(config).toBeUndefined();
});
it('should return undefined when no config files exist in search paths', () => {
// Mock process.cwd to return a directory without config files
vi.spyOn(process, 'cwd').mockReturnValue('/tmp/empty-dir');
const config = loadConfig();
expect(config).toBeUndefined();
});
it('should handle invalid JSON gracefully', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': '{ invalid json }',
});
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
const config = loadConfig();
expect(config).toBeUndefined();
});
it('should prioritize local .ccusage config over Claude paths', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': JSON.stringify({
defaults: { json: true },
commands: { daily: { priority: 'local' } },
}),
});
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
const config = loadConfig();
expect(config).toBeDefined();
expect(config?.defaults?.json).toBe(true);
expect(config?.commands?.daily?.priority).toBe('local');
});
it('should test configuration priority order with multiple files', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': JSON.stringify({
source: 'local',
defaults: { mode: 'local-mode' },
}),
});
// Test 1: Local config should have highest priority
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
const config1 = loadConfig();
expect(config1?.source).toBe(fixture.getPath('.ccusage/ccusage.json'));
expect(config1?.defaults?.mode).toBe('local-mode');
// Test 2: When local doesn't exist, search in Claude paths
await using fixture2 = await createFixture({
'no-ccusage-dir': '',
});
vi.spyOn(process, 'cwd').mockReturnValue(fixture2.getPath());
const config2 = loadConfig();
// Since we can't easily mock getClaudePaths, this test verifies the logic
// In real implementation, first available config would be loaded
expect(config2).toBeUndefined(); // No local .ccusage and no real Claude paths
});
it('should handle getClaudePaths() errors gracefully', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': JSON.stringify({
defaults: { json: true },
source: 'local-fallback',
}),
});
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
// getClaudePaths might throw if no Claude directories exist
const config = loadConfig();
expect(config).toBeDefined();
expect(config?.source).toBe(fixture.getPath('.ccusage/ccusage.json'));
expect(config?.defaults?.json).toBe(true);
});
it('should handle empty configuration file', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': '{}',
});
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
const config = loadConfig();
expect(config).toBeDefined();
expect(config?.defaults).toBeUndefined();
expect(config?.commands).toBeUndefined();
});
it('should validate configuration structure', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': JSON.stringify({
defaults: 'invalid-type', // Should be object
commands: { daily: { instances: true } },
}),
});
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
const config = loadConfig();
expect(config).toBeUndefined();
});
it('should use validateConfigFile internally', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': JSON.stringify({
defaults: { json: true },
commands: { daily: { instances: true } },
}),
'invalid.json': '{ invalid json',
'valid-minimal.json': '{}',
});
// Test validateConfigFile directly
const validResult = validateConfigFile(fixture.getPath('.ccusage/ccusage.json'));
expect(validResult.success).toBe(true);
expect((validResult as { success: true; data: ConfigData }).data.defaults?.json).toBe(true);
expect(
(validResult as { success: true; data: ConfigData }).data.commands?.daily?.instances,
).toBe(true);
const invalidResult = validateConfigFile(fixture.getPath('invalid.json'));
expect(invalidResult.success).toBe(false);
expect((invalidResult as { success: false; error: Error }).error).toBeInstanceOf(Error);
const minimalResult = validateConfigFile(fixture.getPath('valid-minimal.json'));
expect(minimalResult.success).toBe(true);
expect((minimalResult as { success: true; data: ConfigData }).data).toEqual({});
const nonExistentResult = validateConfigFile(fixture.getPath('non-existent.json'));
expect(nonExistentResult.success).toBe(false);
expect((nonExistentResult as { success: false; error: Error }).error.message).toContain(
'does not exist',
);
});
});
describe('mergeConfigWithArgs', () => {
it('should merge config with CLI args correctly', () => {
const config: ConfigData = {
defaults: {
json: false,
mode: 'auto',
debug: false,
},
commands: {
daily: {
instances: true,
project: 'test-project',
},
},
};
const cliArgs = {
json: true, // Override config
project: undefined, // Should not override config
breakdown: true, // Not in config
};
const merged = mergeConfigWithArgs(
{
values: cliArgs,
tokens: [
{ kind: 'option', name: 'json' },
{ kind: 'option', name: 'breakdown' },
],
name: 'daily',
},
config,
);
expect(merged).toEqual({
json: true, // From CLI (overrides config)
mode: 'auto', // From defaults
debug: false, // From defaults
instances: true, // From command config
project: 'test-project', // From command config (CLI was undefined)
breakdown: true, // From CLI (new option)
});
});
it('should work without config', () => {
const cliArgs = { json: true, debug: false };
const merged = mergeConfigWithArgs({
values: cliArgs,
tokens: [
{ kind: 'option', name: 'json' },
{ kind: 'option', name: 'debug' },
],
name: 'daily',
});
expect(merged).toEqual(cliArgs);
});
it('should prioritize CLI args over config', () => {
const config: ConfigData = {
defaults: { json: false },
commands: { daily: { instances: false } },
};
const cliArgs = { json: true, instances: true };
const merged = mergeConfigWithArgs(
{
values: cliArgs,
tokens: [
{ kind: 'option', name: 'json' },
{ kind: 'option', name: 'instances' },
],
name: 'daily',
},
config,
);
expect(merged.json).toBe(true);
expect(merged.instances).toBe(true);
});
it('should not override config with CLI args that were not explicitly provided', () => {
const config: ConfigData = {
defaults: {
json: false,
mode: 'calculate',
},
commands: {
daily: {
instances: true,
},
},
};
// CLI args has values but only 'json' was explicitly provided
const cliArgs = {
json: true,
mode: 'auto', // This has a value but wasn't explicitly provided
instances: false, // This also has a value but wasn't explicitly provided
};
const merged = mergeConfigWithArgs(
{
values: cliArgs,
tokens: [
{ kind: 'option', name: 'json' }, // Only json was explicitly provided
],
name: 'daily',
},
config,
);
expect(merged).toEqual({
json: true, // From CLI (explicitly provided)
mode: 'calculate', // From config (CLI value ignored because not explicit)
instances: true, // From command config (CLI value ignored because not explicit)
});
});
it('should handle CLI args with null values correctly', () => {
const config: ConfigData = {
defaults: {
project: 'default-project',
},
};
const cliArgs = {
project: null, // Explicitly set to null
};
const merged = mergeConfigWithArgs(
{ values: cliArgs, tokens: [{ kind: 'option', name: 'project' }], name: 'daily' },
config,
);
// null value in CLI args should not override config even if explicit
expect(merged).toEqual({
project: 'default-project', // Config value retained because CLI value is null
});
});
});
describe('validateConfigFile', () => {
it('should validate valid config file', async () => {
await using fixture = await createFixture({
'valid.json': JSON.stringify({
defaults: { json: true },
}),
'invalid.json': '{ invalid json',
});
const result = validateConfigFile(fixture.getPath('valid.json'));
expect(result.success).toBe(true);
});
it('should reject invalid JSON', async () => {
await using fixture = await createFixture({
'valid.json': JSON.stringify({
defaults: { json: true },
}),
'invalid.json': '{ invalid json',
});
const result = validateConfigFile(fixture.getPath('invalid.json'));
expect(result.success).toBe(false);
});
it('should reject non-existent file', () => {
const result = validateConfigFile('/non/existent/file.json');
expect(result.success).toBe(false);
});
});
describe('debug functionality', () => {
let loggerInfoSpy: any;
beforeEach(() => {
vi.restoreAllMocks();
loggerInfoSpy = vi.spyOn(logger, 'info').mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('loadConfig with debug', () => {
it('should log debug info when loading config with debug=true', async () => {
await using fixture = await createFixture({
'.ccusage/ccusage.json': JSON.stringify({
$schema: 'https://ccusage.com/config-schema.json',
defaults: { json: true, mode: 'auto' },
commands: { daily: { instances: true } },
}),
});
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
const config = loadConfig(undefined, true);
expect(config).toBeDefined();
expect(loggerInfoSpy).toHaveBeenCalledWith(
'Debug mode enabled - showing config loading details\n',
);
expect(loggerInfoSpy).toHaveBeenCalledWith('Searching for config files:');
expect(loggerInfoSpy).toHaveBeenCalledWith(
` • Checking: ${fixture.getPath('.ccusage/ccusage.json')} (found ✓)`,
);
expect(loggerInfoSpy).toHaveBeenCalledWith('');
expect(loggerInfoSpy).toHaveBeenCalledWith(
`Loaded config from: ${fixture.getPath('.ccusage/ccusage.json')}`,
);
expect(loggerInfoSpy).toHaveBeenCalledWith(
' • Schema: https://ccusage.com/config-schema.json',
);
expect(loggerInfoSpy).toHaveBeenCalledWith(' • Has defaults: yes (2 options)');
expect(loggerInfoSpy).toHaveBeenCalledWith(' • Has command configs: yes (daily)');
});
it('should log search paths when no config found with debug=true', async () => {
await using fixture = await createFixture({
'no-config-here': '',
});
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
const config = loadConfig(undefined, true);
expect(config).toBeUndefined();
expect(loggerInfoSpy).toHaveBeenCalledWith(
'Debug mode enabled - showing config loading details\n',
);
expect(loggerInfoSpy).toHaveBeenCalledWith('Searching for config files:');
expect(loggerInfoSpy).toHaveBeenCalledWith('');
expect(loggerInfoSpy).toHaveBeenCalledWith('No valid configuration file found');
});
it('should log specific config file path when provided', async () => {
await using fixture = await createFixture({
'custom-config.json': JSON.stringify({
defaults: { debug: true },
}),
});
const configPath = fixture.getPath('custom-config.json');
const config = loadConfig(configPath, true);
expect(config).toBeDefined();
expect(loggerInfoSpy).toHaveBeenCalledWith(
'Debug mode enabled - showing config loading details\n',
);
expect(loggerInfoSpy).toHaveBeenCalledWith('Using specified config file:');
expect(loggerInfoSpy).toHaveBeenCalledWith(` • Path: ${configPath}`);
expect(loggerInfoSpy).toHaveBeenCalledWith('');
expect(loggerInfoSpy).toHaveBeenCalledWith(`Loaded config from: ${configPath}`);
expect(loggerInfoSpy).toHaveBeenCalledWith(' • Schema: none');
expect(loggerInfoSpy).toHaveBeenCalledWith(' • Has defaults: yes (1 options)');
expect(loggerInfoSpy).toHaveBeenCalledWith(' • Has command configs: no');
});
});
describe('mergeConfigWithArgs with debug', () => {
it('should log merge details with debug=true', () => {
const config: ConfigData = {
defaults: {
mode: 'auto',
offline: false,
},
commands: {
daily: {
instances: true,
project: 'test-project',
},
},
};
const cliArgs = {
debug: true,
since: '20250101',
};
const merged = mergeConfigWithArgs(
{
values: cliArgs,
tokens: [
{ kind: 'option', name: 'debug' },
{ kind: 'option', name: 'since' },
],
name: 'daily',
},
config,
true,
);
expect(merged).toEqual({
mode: 'auto',
offline: false,
instances: true,
project: 'test-project',
debug: true,
since: '20250101',
});
expect(loggerInfoSpy).toHaveBeenCalledWith('');
expect(loggerInfoSpy).toHaveBeenCalledWith(`Merging options for 'daily' command:`);
expect(loggerInfoSpy).toHaveBeenCalledWith(' • From defaults: mode="auto", offline=false');
expect(loggerInfoSpy).toHaveBeenCalledWith(
' • From command config: instances=true, project="test-project"',
);
expect(loggerInfoSpy).toHaveBeenCalledWith(
' • From CLI args: debug=true, since="20250101"',
);
expect(loggerInfoSpy).toHaveBeenCalledWith(' • Final merged options: {');
});
it('should log no config message with debug=true when config is null', () => {
const cliArgs = { json: true, debug: false };
const merged = mergeConfigWithArgs(
{
values: cliArgs,
tokens: [
{ kind: 'option', name: 'json' },
{ kind: 'option', name: 'debug' },
],
name: 'daily',
},
undefined,
true,
);
expect(merged).toEqual(cliArgs);
expect(loggerInfoSpy).toHaveBeenCalledWith('');
expect(loggerInfoSpy).toHaveBeenCalledWith(
`No config file loaded, using CLI args only for 'daily' command`,
);
});
});
});
}
================================================
FILE: apps/ccusage/src/_consts.ts
================================================
import { homedir } from 'node:os';
import path from 'node:path';
import { xdgConfig } from 'xdg-basedir';
/**
* Default number of recent days to include when filtering blocks
* Used in both session blocks and commands for consistent behavior
*/
export const DEFAULT_RECENT_DAYS = 3;
/**
* Threshold percentage for showing usage warnings in blocks command (80%)
* When usage exceeds this percentage of limits, warnings are displayed
*/
export const BLOCKS_WARNING_THRESHOLD = 0.8;
/**
* Terminal width threshold for switching to compact display mode in blocks command
* Below this width, tables use more compact formatting
*/
export const BLOCKS_COMPACT_WIDTH_THRESHOLD = 120;
/**
* Default terminal width when stdout.columns is not available in blocks command
* Used as fallback for responsive table formatting
*/
export const BLOCKS_DEFAULT_TERMINAL_WIDTH = 120;
/**
* Threshold percentage for considering costs as matching (0.1% tolerance)
* Used in debug cost validation to allow for minor calculation differences
*/
export const DEBUG_MATCH_THRESHOLD_PERCENT = 0.1;
/**
* User's home directory path
* Centralized access to OS home directory for consistent path building
*/
export const USER_HOME_DIR = homedir();
/**
* XDG config directory path
* Uses XDG_CONFIG_HOME if set, otherwise falls back to ~/.config
*/
const XDG_CONFIG_DIR = xdgConfig ?? path.join(USER_HOME_DIR, '.config');
/**
* Default Claude data directory path (~/.claude)
* Used as base path for loading usage data from JSONL files
*/
export const DEFAULT_CLAUDE_CODE_PATH = '.claude';
/**
* Default Claude data directory path using XDG config directory
* Uses XDG_CONFIG_HOME if set, otherwise falls back to ~/.config/claude
*/
export const DEFAULT_CLAUDE_CONFIG_PATH = path.join(XDG_CONFIG_DIR, 'claude');
/**
* Environment variable for specifying multiple Claude data directories
* Supports comma-separated paths for multiple locations
*/
export const CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR';
/**
* Claude projects directory name within the data directory
* Contains subdirectories for each project with usage data
*/
export const CLAUDE_PROJECTS_DIR_NAME = 'projects';
/**
* JSONL file glob pattern for finding usage data files
* Used to recursively find all JSONL files in project directories
*/
export const USAGE_DATA_GLOB_PATTERN = '**/*.jsonl';
/**
* Default port for MCP server HTTP transport
* Used when no port is specified for MCP server communication
*/
export const MCP_DEFAULT_PORT = 8080;
/**
* Default refresh interval in seconds for statusline cache expiry
*/
export const DEFAULT_REFRESH_INTERVAL_SECONDS = 1;
/**
* Context usage percentage thresholds for color coding
*/
export const DEFAULT_CONTEXT_USAGE_THRESHOLDS = {
LOW: 50, // Below 50% - green
MEDIUM: 80, // 50-80% - yellow
// Above 80% - red
} as const;
/**
* Days of the week for weekly aggregation
*/
export const WEEK_DAYS = [
'sunday',
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
] as const;
/**
* Week day names type
*/
export type WeekDay = (typeof WEEK_DAYS)[number];
/**
* Day of week as number (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
*/
export type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6;
/**
* Default configuration file name for storing usage data
* Used to load and save configuration settings
*/
export const CONFIG_FILE_NAME = 'ccusage.json';
/**
* Default locale for date formatting (en-CA provides YYYY-MM-DD ISO format)
* Used consistently across the application for date parsing and display
*/
export const DEFAULT_LOCALE = 'en-CA';
================================================
FILE: apps/ccusage/src/_daily-grouping.ts
================================================
import type { DailyProjectOutput } from './_json-output-types.ts';
import type { loadDailyUsageData } from './data-loader.ts';
import { createDailyDate, createModelName } from './_types.ts';
import { getTotalTokens } from './calculate-cost.ts';
/**
* Type for daily data returned from loadDailyUsageData
*/
type DailyData = Awaited<ReturnType<typeof loadDailyUsageData>>;
/**
* Group daily usage data by project for JSON output
*/
export function groupByProject(dailyData: DailyData): Record<string, DailyProjectOutput[]> {
const projects: Record<string, DailyProjectOutput[]> = {};
for (const data of dailyData) {
const projectName = data.project ?? 'unknown';
if (projects[projectName] == null) {
projects[projectName] = [];
}
projects[projectName].push({
date: data.date,
inputTokens: data.inputTokens,
outputTokens: data.outputTokens,
cacheCreationTokens: data.cacheCreationTokens,
cacheReadTokens: data.cacheReadTokens,
totalTokens: getTotalTokens(data),
totalCost: data.totalCost,
modelsUsed: data.modelsUsed,
modelBreakdowns: data.modelBreakdowns,
});
}
return projects;
}
/**
* Group daily usage data by project for table display
*/
export function groupDataByProject(dailyData: DailyData): Record<string, DailyData> {
const projects: Record<string, DailyData> = {};
for (const data of dailyData) {
const projectName = data.project ?? 'unknown';
if (projects[projectName] == null) {
projects[projectName] = [];
}
projects[projectName].push(data);
}
return projects;
}
if (import.meta.vitest != null) {
describe('groupByProject', () => {
it('groups daily data by project for JSON output', () => {
const mockData = [
{
date: createDailyDate('2024-01-01'),
project: 'project-a',
inputTokens: 1000,
outputTokens: 500,
cacheCreationTokens: 100,
cacheReadTokens: 200,
totalCost: 0.01,
modelsUsed: [createModelName('claude-sonnet-4-20250514')],
modelBreakdowns: [],
},
{
date: createDailyDate('2024-01-01'),
project: 'project-b',
inputTokens: 2000,
outputTokens: 1000,
cacheCreationTokens: 200,
cacheReadTokens: 300,
totalCost: 0.02,
modelsUsed: [createModelName('claude-opus-4-20250514')],
modelBreakdowns: [],
},
];
const result = groupByProject(mockData);
expect(Object.keys(result)).toHaveLength(2);
expect(result['project-a']).toHaveLength(1);
expect(result['project-b']).toHaveLength(1);
expect(result['project-a']![0]!.totalTokens).toBe(1800);
expect(result['project-b']![0]!.totalTokens).toBe(3500);
});
it('handles unknown project names', () => {
const mockData = [
{
date: createDailyDate('2024-01-01'),
project: undefined,
inputTokens: 1000,
outputTokens: 500,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalCost: 0.01,
modelsUsed: [createModelName('claude-sonnet-4-20250514')],
modelBreakdowns: [],
},
];
const result = groupByProject(mockData);
expect(Object.keys(result)).toHaveLength(1);
expect(result.unknown).toHaveLength(1);
});
});
describe('groupDataByProject', () => {
it('groups daily data by project for table display', () => {
const mockData = [
{
date: createDailyDate('2024-01-01'),
project: 'project-a',
inputTokens: 1000,
outputTokens: 500,
cacheCreationTokens: 100,
cacheReadTokens: 200,
totalCost: 0.01,
modelsUsed: [createModelName('claude-sonnet-4-20250514')],
modelBreakdowns: [],
},
{
date: createDailyDate('2024-01-02'),
project: 'project-a',
inputTokens: 800,
outputTokens: 400,
cacheCreationTokens: 50,
cacheReadTokens: 150,
totalCost: 0.008,
modelsUsed: [createModelName('claude-sonnet-4-20250514')],
modelBreakdowns: [],
},
];
const result = groupDataByProject(mockData);
expect(Object.keys(result)).toHaveLength(1);
expect(result['project-a']).toHaveLength(2);
expect(result['project-a']).toEqual(mockData);
});
});
}
================================================
FILE: apps/ccusage/src/_date-utils.ts
================================================
/**
* Date utility functions for handling date formatting, filtering, and manipulation
* @module date-utils
*/
import type { DayOfWeek, WeekDay } from './_consts.ts';
import type { WeeklyDate } from './_types.ts';
import { sort } from 'fast-sort';
import { DEFAULT_LOCALE } from './_consts.ts';
import { createWeeklyDate } from './_types.ts';
import { unreachable } from './_utils.ts';
// Re-export formatDateCompact from shared package
export { formatDateCompact } from '@ccusage/terminal/table';
/**
* Sort order for date-based sorting
*/
export type SortOrder = 'asc' | 'desc';
/**
* Creates a date formatter with the specified timezone and locale
* @param timezone - Timezone to use (e.g., 'UTC', 'America/New_York')
* @param locale - Locale to use for formatting (e.g., 'en-US', 'ja-JP')
* @returns Intl.DateTimeFormat instance
*/
function createDateFormatter(timezone: string | undefined, locale: string): Intl.DateTimeFormat {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
timeZone: timezone,
});
}
/**
* Formats a date string to YYYY-MM-DD format
* @param dateStr - Input date string
* @param timezone - Optional timezone to use for formatting
* @param locale - Optional locale to use for formatting (defaults to DEFAULT_LOCALE for YYYY-MM-DD format)
* @returns Formatted date string in YYYY-MM-DD format
*/
export function formatDate(dateStr: string, timezone?: string, locale?: string): string {
const date = new Date(dateStr);
// Use DEFAULT_LOCALE as default for consistent YYYY-MM-DD format
const formatter = createDateFormatter(timezone, locale ?? DEFAULT_LOCALE);
return formatter.format(date);
}
/**
* Generic function to sort items by date based on sort order
* @param items - Array of items to sort
* @param getDate - Function to extract date/timestamp from item
* @param order - Sort order (asc or desc)
* @returns Sorted array
*/
export function sortByDate<T>(
items: T[],
getDate: (item: T) => string | Date,
order: SortOrder = 'desc',
): T[] {
const sorted = sort(items);
switch (order) {
case 'desc':
return sorted.desc((item) => new Date(getDate(item)).getTime());
case 'asc':
return sorted.asc((item) => new Date(getDate(item)).getTime());
default:
unreachable(order);
}
}
/**
* Filters items by date range
* @param items - Array of items to filter
* @param getDate - Function to extract date string from item
* @param since - Start date in any format (will be converted to YYYYMMDD for comparison)
* @param until - End date in any format (will be converted to YYYYMMDD for comparison)
* @returns Filtered array
*/
export function filterByDateRange<T>(
items: T[],
getDate: (item: T) => string,
since?: string,
until?: string,
): T[] {
if (since == null && until == null) {
return items;
}
return items.filter((item) => {
const dateStr = getDate(item).substring(0, 10).replace(/-/g, ''); // Convert to YYYYMMDD
if (since != null && dateStr < since) {
return false;
}
if (until != null && dateStr > until) {
return false;
}
return true;
});
}
/**
* Get the first day of the week for a given date
* @param date - The date to get the week for
* @param startDay - The day to start the week on (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
* @returns The date of the first day of the week for the given date
*/
export function getDateWeek(date: Date, startDay: DayOfWeek): WeeklyDate {
const d = new Date(date);
const day = d.getDay();
const shift = (day - startDay + 7) % 7;
d.setDate(d.getDate() - shift);
return createWeeklyDate(d.toISOString().substring(0, 10));
}
/**
* Convert day name to number (0 = Sunday, 1 = Monday, ..., 6 = Saturday)
* @param day - Day name
* @returns Day number
*/
export function getDayNumber(day: WeekDay): DayOfWeek {
const dayMap = {
sunday: 0,
monday: 1,
tuesday: 2,
wednesday: 3,
thursday: 4,
friday: 5,
saturday: 6,
} as const satisfies Record<WeekDay, DayOfWeek>;
return dayMap[day];
}
if (import.meta.vitest != null) {
describe('formatDate', () => {
it('should format date string to YYYY-MM-DD format', () => {
const result = formatDate('2024-08-04T12:00:00Z');
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
it('should handle timezone parameter', () => {
const result = formatDate('2024-08-04T12:00:00Z', 'UTC');
expect(result).toBe('2024-08-04');
});
it('should use default locale when locale is not provided', () => {
const result = formatDate('2024-08-04T12:00:00Z');
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
it('should handle custom locale', () => {
const result = formatDate('2024-08-04T12:00:00Z', 'UTC', 'en-US');
expect(result).toBe('08/04/2024');
});
});
// formatDateCompact tests are in @ccusage/terminal/table.ts
describe('sortByDate', () => {
const testData = [
{ id: 1, date: '2024-01-01T10:00:00Z' },
{ id: 2, date: '2024-01-03T10:00:00Z' },
{ id: 3, date: '2024-01-02T10:00:00Z' },
];
it('should sort by date in descending order by default', () => {
const result = sortByDate(testData, (item) => item.date);
expect(result.map((item) => item.id)).toEqual([2, 3, 1]);
});
it('should sort by date in ascending order when specified', () => {
const result = sortByDate(testData, (item) => item.date, 'asc');
expect(result.map((item) => item.id)).toEqual([1, 3, 2]);
});
it('should sort by date in descending order when explicitly specified', () => {
const result = sortByDate(testData, (item) => item.date, 'desc');
expect(result.map((item) => item.id)).toEqual([2, 3, 1]);
});
it('should handle Date objects', () => {
const dateData = [
{ id: 1, date: new Date('2024-01-01T10:00:00Z') },
{ id: 2, date: new Date('2024-01-03T10:00:00Z') },
{ id: 3, date: new Date('2024-01-02T10:00:00Z') },
];
const result = sortByDate(dateData, (item) => item.date);
expect(result.map((item) => item.id)).toEqual([2, 3, 1]);
});
});
describe('filterByDateRange', () => {
const testData = [
{ id: 1, date: '2024-01-01' },
{ id: 2, date: '2024-01-02' },
{ id: 3, date: '2024-01-03' },
{ id: 4, date: '2024-01-04' },
{ id: 5, date: '2024-01-05' },
];
it('should return all items when no date filters are provided', () => {
const result = filterByDateRange(testData, (item) => item.date);
expect(result).toEqual(testData);
});
it('should filter by since date', () => {
const result = filterByDateRange(testData, (item) => item.date, '20240103');
expect(result.map((item) => item.id)).toEqual([3, 4, 5]);
});
it('should filter by until date', () => {
const result = filterByDateRange(testData, (item) => item.date, undefined, '20240103');
expect(result.map((item) => item.id)).toEqual([1, 2, 3]);
});
it('should filter by both since and until dates', () => {
const result = filterByDateRange(testData, (item) => item.date, '20240102', '20240104');
expect(result.map((item) => item.id)).toEqual([2, 3, 4]);
});
it('should handle timestamp format dates', () => {
const timestampData = [
{ id: 1, date: '2024-01-01T10:00:00Z' },
{ id: 2, date: '2024-01-02T10:00:00Z' },
{ id: 3, date: '2024-01-03T10:00:00Z' },
];
const result = filterByDateRange(timestampData, (item) => item.date, '20240102');
expect(result.map((item) => item.id)).toEqual([2, 3]);
});
});
describe('getDateWeek', () => {
it('should get the first day of week starting from Sunday', () => {
const date = new Date('2024-01-03T10:00:00Z'); // Wednesday
const result = getDateWeek(date, 0); // Sunday start
expect(result).toBe(createWeeklyDate('2023-12-31')); // Previous Sunday
});
it('should get the first day of week starting from Monday', () => {
const date = new Date('2024-01-03T10:00:00Z'); // Wednesday
const result = getDateWeek(date, 1); // Monday start
expect(result).toBe(createWeeklyDate('2024-01-01')); // Monday of same week
});
it('should handle when the date is already the start of the week', () => {
const date = new Date('2024-01-01T10:00:00Z'); // Monday
const result = getDateWeek(date, 1); // Monday start
expect(result).toBe(createWeeklyDate('2024-01-01')); // Same Monday
});
it('should handle Sunday as start of week when date is Sunday', () => {
const date = new Date('2023-12-31T10:00:00Z'); // Sunday
const result = getDateWeek(date, 0); // Sunday start
expect(result).toBe(createWeeklyDate('2023-12-31')); // Same Sunday
});
});
describe('getDayNumber', () => {
it('should convert day names to correct numbers', () => {
expect(getDayNumber('sunday')).toBe(0);
expect(getDayNumber('monday')).toBe(1);
expect(getDayNumber('tuesday')).toBe(2);
expect(getDayNumber('wednesday')).toBe(3);
expect(getDayNumber('thursday')).toBe(4);
expect(getDayNumber('friday')).toBe(5);
expect(getDayNumber('saturday')).toBe(6);
});
});
}
================================================
FILE: apps/ccusage/src/_jq-processor.ts
================================================
import { Result } from '@praha/byethrow';
import spawn from 'nano-spawn';
/**
* Process JSON data with a jq command
* @param jsonData - The JSON data to process
* @param jqCommand - The jq command/filter to apply
* @returns The processed output from jq
*/
export async function processWithJq(
jsonData: unknown,
jqCommand: string,
): Result.ResultAsync<string, Error> {
// Convert JSON data to string
const jsonString = JSON.stringify(jsonData);
// Use Result.try with object form to wrap spawn call
const result = Result.try({
try: async () => {
const spawnResult = await spawn('jq', [jqCommand], {
stdin: { string: jsonString },
});
return spawnResult.output.trim();
},
catch: (error: unknown) => {
if (error instanceof Error) {
// Check if jq is not installed
if (error.message.includes('ENOENT') || error.message.includes('not found')) {
return new Error('jq command not found. Please install jq to use the --jq option.');
}
// Return other errors (e.g., invalid jq syntax)
return new Error(`jq processing failed: ${error.message}`);
}
return new Error('Unknown error during jq processing');
},
});
return result();
}
// In-source tests
if (import.meta.vitest != null) {
describe('processWithJq', () => {
it('should process JSON with simple filter', async () => {
const data = { name: 'test', value: 42 };
const result = await processWithJq(data, '.name');
const unwrapped = Result.unwrap(result);
expect(unwrapped).toBe('"test"');
});
it('should process JSON with complex filter', async () => {
const data = {
items: [
{ id: 1, name: 'apple' },
{ id: 2, name: 'banana' },
],
};
const result = await processWithJq(data, '.items | map(.name)');
const unwrapped = Result.unwrap(result);
const parsed = JSON.parse(unwrapped) as string[];
expect(parsed).toEqual(['apple', 'banana']);
});
it('should handle raw output', async () => {
const data = { message: 'hello world' };
const result = await processWithJq(data, '.message | @text');
const unwrapped = Result.unwrap(result);
expect(unwrapped).toBe('"hello world"');
});
it('should return error for invalid jq syntax', async () => {
const data = { test: 'value' };
const result = await processWithJq(data, 'invalid syntax {');
const error = Result.unwrapError(result);
expect(error.message).toContain('jq processing failed');
});
it('should handle complex jq operations', async () => {
const data = {
users: [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
{ name: 'Charlie', age: 35 },
],
};
const result = await processWithJq(data, '.users | sort_by(.age) | .[0].name');
const unwrapped = Result.unwrap(result);
expect(unwrapped).toBe('"Bob"');
});
it('should handle numeric output', async () => {
const data = { values: [1, 2, 3, 4, 5] };
const result = await processWithJq(data, '.values | add');
const unwrapped = Result.unwrap(result);
expect(unwrapped).toBe('15');
});
});
}
================================================
FILE: apps/ccusage/src/_json-output-types.ts
================================================
/**
* @fileoverview JSON output interface types for daily command groupByProject function
*
* This module provides TypeScript interfaces for the JSON output structure
* used by the daily command's groupByProject function, replacing the
* unsafe Record<string, any[]> type with proper type definitions.
*
* @module json-output-types
*/
import type { DailyDate, ModelName } from './_types.ts';
import type { ModelBreakdown } from './data-loader.ts';
/**
* Interface for daily command JSON output structure (groupByProject)
* Used in src/commands/daily.ts
*/
export type DailyProjectOutput = {
date: DailyDate;
inputTokens: number;
outputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
totalTokens: number;
totalCost: number;
modelsUsed: ModelName[];
modelBreakdowns: ModelBreakdown[];
};
================================================
FILE: apps/ccusage/src/_macro.ts
================================================
import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';
import {
createPricingDataset,
fetchLiteLLMPricingDataset,
filterPricingDataset,
} from '@ccusage/internal/pricing-fetch-utils';
function isClaudeModel(modelName: string, _pricing: LiteLLMModelPricing): boolean {
return (
modelName.startsWith('claude-') ||
modelName.startsWith('anthropic.claude-') ||
modelName.startsWith('anthropic/claude-')
);
}
export async function prefetchClaudePricing(): Promise<Record<string, LiteLLMModelPricing>> {
try {
const dataset = await fetchLiteLLMPricingDataset();
return filterPricingDataset(dataset, isClaudeModel);
} catch (error) {
console.warn('Failed to prefetch Claude pricing data, proceeding with empty cache.', error);
return createPricingDataset();
}
}
================================================
FILE: apps/ccusage/src/_pricing-fetcher.ts
================================================
import { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';
import { Result } from '@praha/byethrow';
import { prefetchClaudePricing } from './_macro.ts' with { type: 'macro' };
import { logger } from './logger.ts';
const CLAUDE_PROVIDER_PREFIXES = [
'anthropic/',
'claude-3-5-',
'claude-3-',
'claude-',
'openrouter/openai/',
];
const PREFETCHED_CLAUDE_PRICING = prefetchClaudePricing();
export class PricingFetcher extends LiteLLMPricingFetcher {
constructor(offline = false) {
super({
offline,
offlineLoader: async () => PREFETCHED_CLAUDE_PRICING,
logger,
providerPrefixes: CLAUDE_PROVIDER_PREFIXES,
});
}
}
if (import.meta.vitest != null) {
describe('PricingFetcher', () => {
it('loads offline pricing when offline flag is true', async () => {
using fetcher = new PricingFetcher(true);
const pricing = await Result.unwrap(fetcher.fetchModelPricing());
expect(pricing.size).toBeGreaterThan(0);
});
it('calculates cost for Claude model tokens', async () => {
using fetcher = new PricingFetcher(true);
const pricing = await Result.unwrap(fetcher.getModelPricing('claude-sonnet-4-20250514'));
const cost = fetcher.calculateCostFromPricing(
{
input_tokens: 1000,
output_tokens: 500,
cache_read_input_tokens: 300,
},
pricing!,
);
expect(cost).toBeGreaterThan(0);
});
});
}
================================================
FILE: apps/ccusage/src/_project-names.ts
================================================
/**
* @fileoverview Project name formatting and alias utilities
*
* Provides utilities for formatting raw project directory names into user-friendly
* display names with support for custom aliases and improved path parsing.
*
* @module project-names
*/
/**
* Extract meaningful project name from directory-style project paths
* Uses improved heuristics to handle complex project structures
*
* @param projectName - Raw project name from directory path
* @returns Cleaned and formatted project name
*
* @example
* ```typescript
* // Basic cleanup
* parseProjectName('-Users-phaedrus-Development-ccusage')
* // → 'ccusage'
*
* // Complex project with feature branch
* parseProjectName('-Users-phaedrus-Development-adminifi-edugakko-api--feature-ticket-002-configure-dependabot')
* // → 'configure-dependabot'
*
* // Handle unknown projects
* parseProjectName('unknown')
* // → 'Unknown Project'
* ```
*/
function parseProjectName(projectName: string): string {
if (projectName === 'unknown' || projectName === '') {
return 'Unknown Project';
}
// Remove common directory prefixes
let cleaned = projectName;
// Handle Windows-style paths: C:\Users\... or \Users\...
if (cleaned.match(/^[A-Z]:\\Users\\|^\\Users\\/) != null) {
const segments = cleaned.split('\\');
const userIndex = segments.findIndex((seg) => seg === 'Users');
if (userIndex !== -1 && userIndex + 3 < segments.length) {
// Take everything after Users/username/Projects or similar
cleaned = segments.slice(userIndex + 3).join('-');
}
}
// Handle Unix-style paths: /Users/... or -Users-...
if (cleaned.startsWith('-Users-') || cleaned.startsWith('/Users/')) {
const separator = cleaned.startsWith('-Users-') ? '-' : '/';
const segments = cleaned.split(separator).filter((s) => s.length > 0);
const userIndex = segments.findIndex((seg) => seg === 'Users');
if (userIndex !== -1 && userIndex + 3 < segments.length) {
// Take everything after Users/username/Development or similar
cleaned = segments.slice(userIndex + 3).join('-');
}
}
// If no path cleanup occurred, use original name
if (cleaned === projectName) {
// Just basic cleanup for non-path names
cleaned = projectName.replace(/^[/\\-]+|[/\\-]+$/g, '');
}
// Handle UUID-like patterns
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) {
// Extract last two segments of UUID for brevity
const parts = cleaned.split('-');
if (parts.length >= 5) {
// Take the last two segments, which may include file extension in the last segment
cleaned = parts.slice(-2).join('-');
}
}
// Improved project name extraction for complex names
if (cleaned.includes('--')) {
// Handle project--feature patterns like "adminifi-edugakko-api--feature-ticket-002"
const parts = cleaned.split('--');
if (parts.length >= 2 && parts[0] != null) {
// Take the main project part before the first --
cleaned = parts[0];
}
}
// For compound project names, try to extract the most meaningful part
if (cleaned.includes('-') && cleaned.length > 20) {
const segments = cleaned.split('-');
// Look for common meaningful patterns
const meaningfulSegments = segments.filter(
(seg) =>
seg.length > 2 &&
seg.match(
/^(?:dev|development|feat|feature|fix|bug|test|staging|prod|production|main|master|branch)$/i,
) == null,
);
// If we have compound project names like "adminifi-edugakko-api"
// Try to find the last 2-3 meaningful segments
if (meaningfulSegments.length >= 2) {
// Take last 2-3 segments to get "edugakko-api" from "adminifi-edugakko-api"
const lastSegments = meaningfulSegments.slice(-2);
if (lastSegments.join('-').length >= 6) {
cleaned = lastSegments.join('-');
} else if (meaningfulSegments.length >= 3) {
cleaned = meaningfulSegments.slice(-3).join('-');
}
}
}
// Final cleanup
cleaned = cleaned.replace(/^[/\\-]+|[/\\-]+$/g, '');
return cleaned !== '' ? cleaned : projectName !== '' ? projectName : 'Unknown Project';
}
/**
* Format project name for display with custom alias support
*
* @param projectName - Raw project name from directory path
* @param aliases - Optional map of project names to their aliases
* @returns User-friendly project name with alias support
*
* @example
* ```typescript
* // Without aliases
* formatProjectName('-Users-phaedrus-Development-ccusage')
* // → 'ccusage'
*
* // With alias
* const aliases = new Map([['ccusage', 'Usage Tracker']]);
* formatProjectName('-Users-phaedrus-Development-ccusage', aliases)
* // → 'Usage Tracker'
* ```
*/
export function formatProjectName(projectName: string, aliases?: Map<string, string>): string {
// Check for custom alias first
if (aliases != null && aliases.has(projectName)) {
return aliases.get(projectName)!;
}
// Parse the project name using improved logic
const parsed = parseProjectName(projectName);
// Check if parsed name has an alias
if (aliases != null && aliases.has(parsed)) {
return aliases.get(parsed)!;
}
return parsed;
}
if (import.meta.vitest != null) {
const { describe, it, expect } = import.meta.vitest;
describe('project name formatting', () => {
describe('parseProjectName', () => {
it('handles unknown project names', () => {
expect(formatProjectName('unknown')).toBe('Unknown Project');
expect(formatProjectName('')).toBe('Unknown Project');
});
it('extracts project names from Unix-style paths', () => {
expect(formatProjectName('-Users-phaedrus-Development-ccusage')).toBe('ccusage');
expect(formatProjectName('/Users/phaedrus/Development/ccusage')).toBe('ccusage');
});
it('handles complex project names with features', () => {
const complexName =
'-Users-phaedrus-Development-adminifi-edugakko-api--feature-ticket-002-configure-dependabot';
const result = formatProjectName(complexName);
// Current logic processes the name and extracts meaningful segments
expect(result).toBe('configure-dependabot');
});
it('handles UUID-based project names', () => {
const uuidName = 'a2cd99ed-a586-4fe4-8f59-b0026409ec09.jsonl';
const result = formatProjectName(uuidName);
expect(result).toBe('8f59-b0026409ec09.jsonl');
});
it('returns original name for simple names', () => {
expect(formatProjectName('simple-project')).toBe('simple-project');
expect(formatProjectName('project')).toBe('project');
});
});
describe('custom aliases', () => {
it('uses configured aliases', () => {
const aliases = new Map([
['ccusage', 'Usage Tracker'],
['test', 'Test Project'],
]);
expect(formatProjectName('ccusage', aliases)).toBe('Usage Tracker');
expect(formatProjectName('test', aliases)).toBe('Test Project');
expect(formatProjectName('other', aliases)).toBe('other');
});
it('applies aliases to parsed project names', () => {
const aliases = new Map([['ccusage', 'Usage Tracker']]);
expect(formatProjectName('-Users-phaedrus-Development-ccusage', aliases)).toBe(
'Usage Tracker',
);
});
it('works without aliases', () => {
expect(formatProjectName('test')).toBe('test');
expect(formatProjectName('test', undefined)).toBe('test');
expect(formatProjectName('test', new Map())).toBe('test');
});
});
});
}
================================================
FILE: apps/ccusage/src/_session-blocks.ts
================================================
import { uniq } from 'es-toolkit';
import { DEFAULT_RECENT_DAYS } from './_consts.ts';
import { getTotalTokens } from './_token-utils.ts';
/**
* Default session duration in hours (Claude's billing block duration)
*/
export const DEFAULT_SESSION_DURATION_HOURS = 5;
/**
* Floors a timestamp to the beginning of the hour in UTC
* @param timestamp - The timestamp to floor
* @returns New Date object floored to the UTC hour
*/
function floorToHour(timestamp: Date): Date {
const floored = new Date(timestamp);
floored.setUTCMinutes(0, 0, 0);
return floored;
}
/**
* Represents a single usage data entry loaded from JSONL files
*/
export type LoadedUsageEntry = {
timestamp: Date;
usage: {
inputTokens: number;
outputTokens: number;
cacheCreationInputTokens: number;
cacheReadInputTokens: number;
};
costUSD: number | null;
model: string;
version?: string;
usageLimitResetTime?: Date; // Claude API usage limit reset time
};
/**
* Aggregated token counts for different token types
*/
type TokenCounts = {
inputTokens: number;
outputTokens: number;
cacheCreationInputTokens: number;
cacheReadInputTokens: number;
};
/**
* Represents a session block (typically 5-hour billing period) with usage data
*/
export type SessionBlock = {
id: string; // ISO string of block start time
startTime: Date;
endTime: Date; // startTime + 5 hours (for normal blocks) or gap end time (for gap blocks)
actualEndTime?: Date; // Last activity in block
isActive: boolean;
isGap?: boolean; // True if this is a gap block
entries: LoadedUsageEntry[];
tokenCounts: TokenCounts;
costUSD: number;
models: string[];
usageLimitResetTime?: Date; // Claude API usage limit reset time
};
/**
* Represents usage burn rate calculations
*/
type BurnRate = {
tokensPerMinute: number;
tokensPerMinuteForIndicator: number;
costPerHour: number;
};
/**
* Represents projected usage for remaining time in a session block
*/
type ProjectedUsage = {
totalTokens: number;
totalCost: number;
remainingMinutes: number;
};
/**
* Identifies and creates session blocks from usage entries
* Groups entries into time-based blocks (typically 5-hour periods) with gap detection
* @param entries - Array of usage entries to process
* @param sessionDurationHours - Duration of each session block in hours
* @returns Array of session blocks with aggregated usage data
*/
export function identifySessionBlocks(
entries: LoadedUsageEntry[],
sessionDurationHours = DEFAULT_SESSION_DURATION_HOURS,
): SessionBlock[] {
if (entries.length === 0) {
return [];
}
const sessionDurationMs = sessionDurationHours * 60 * 60 * 1000;
const blocks: SessionBlock[] = [];
const sortedEntries = [...entries].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
let currentBlockStart: Date | null = null;
let currentBlockEntries: LoadedUsageEntry[] = [];
const now = new Date();
for (const entry of sortedEntries) {
const entryTime = entry.timestamp;
if (currentBlockStart == null) {
// First entry - start a new block (floored to the hour)
currentBlockStart = floorToHour(entryTime);
currentBlockEntries = [entry];
} else {
const timeSinceBlockStart = entryTime.getTime() - currentBlockStart.getTime();
const lastEntry = currentBlockEntries.at(-1);
if (lastEntry == null) {
continue;
}
const lastEntryTime = lastEntry.timestamp;
const timeSinceLastEntry = entryTime.getTime() - lastEntryTime.getTime();
if (timeSinceBlockStart > sessionDurationMs || timeSinceLastEntry > sessionDurationMs) {
// Close current block
const block = createBlock(currentBlockStart, currentBlockEntries, now, sessionDurationMs);
blocks.push(block);
// Add gap block if there's a significant gap
if (timeSinceLastEntry > sessionDurationMs) {
const gapBlock = createGapBlock(lastEntryTime, entryTime, sessionDurationMs);
if (gapBlock != null) {
blocks.push(gapBlock);
}
}
// Start new block (floored to the hour)
currentBlockStart = floorToHour(entryTime);
currentBlockEntries = [entry];
} else {
// Add to current block
currentBlockEntries.push(entry);
}
}
}
// Close the last block
if (currentBlockStart != null && currentBlockEntries.length > 0) {
const block = createBlock(currentBlockStart, currentBlockEntries, now, sessionDurationMs);
blocks.push(block);
}
return blocks;
}
/**
* Creates a session block from a start time and usage entries
* @param startTime - When the block started
* @param entries - Usage entries in this block
* @param now - Current time for active block detection
* @param sessionDurationMs - Session duration in milliseconds
* @returns Session block with aggregated data
*/
function createBlock(
startTime: Date,
entries: LoadedUsageEntry[],
now: Date,
sessionDurationMs: number,
): SessionBlock {
const endTime = new Date(startTime.getTime() + sessionDurationMs);
const lastEntry = entries[entries.length - 1];
const actualEndTime = lastEntry != null ? lastEntry.timestamp : startTime;
const isActive = now.getTime() - actualEndTime.getTime() < sessionDurationMs && now < endTime;
// Aggregate token counts
const tokenCounts: TokenCounts = {
inputTokens: 0,
outputTokens: 0,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
};
let costUSD = 0;
const models: string[] = [];
let usageLimitResetTime: Date | undefined;
for (const entry of entries) {
tokenCounts.inputTokens += entry.usage.inputTokens;
tokenCounts.outputTokens += entry.usage.outputTokens;
tokenCounts.cacheCreationInputTokens += entry.usage.cacheCreationInputTokens;
tokenCounts.cacheReadInputTokens += entry.usage.cacheReadInputTokens;
costUSD += entry.costUSD ?? 0;
usageLimitResetTime = entry.usageLimitResetTime ?? usageLimitResetTime;
models.push(entry.model);
}
return {
id: startTime.toISOString(),
startTime,
endTime,
actualEndTime,
isActive,
entries,
tokenCounts,
costUSD,
models: uniq(models),
usageLimitResetTime,
};
}
/**
* Creates a gap block representing periods with no activity
* @param lastActivityTime - Time of last activity before gap
* @param nextActivityTime - Time of next activity after gap
* @param sessionDurationMs - Session duration in milliseconds
* @returns Gap block or null if gap is too short
*/
function createGapBlock(
lastActivityTime: Date,
nextActivityTime: Date,
sessionDurationMs: number,
): SessionBlock | null {
// Only create gap blocks for gaps longer than the session duration
const gapDuration = nextActivityTime.getTime() - lastActivityTime.getTime();
if (gapDuration <= sessionDurationMs) {
return null;
}
const gapStart = new Date(lastActivityTime.getTime() + sessionDurationMs);
const gapEnd = nextActivityTime;
return {
id: `gap-${gapStart.toISOString()}`,
startTime: gapStart,
endTime: gapEnd,
isActive: false,
isGap: true,
entries: [],
tokenCounts: {
inputTokens: 0,
outputTokens: 0,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
},
costUSD: 0,
models: [],
};
}
/**
* Calculates the burn rate (tokens/minute and cost/hour) for a session block
* @param block - Session block to analyze
* @returns Burn rate calculations or null if block has no activity
*/
export function calculateBurnRate(block: SessionBlock): BurnRate | null {
if (block.entries.length === 0 || (block.isGap ?? false)) {
return null;
}
const firstEntryData = block.entries[0];
const lastEntryData = block.entries[block.entries.length - 1];
if (firstEntryData == null || lastEntryData == null) {
return null;
}
const firstEntry = firstEntryData.timestamp;
const lastEntry = lastEntryData.timestamp;
const durationMinutes = (lastEntry.getTime() - firstEntry.getTime()) / (1000 * 60);
if (durationMinutes <= 0) {
return null;
}
const totalTokens =
gitextract_lintubch/ ├── .claude/ │ ├── commands/ │ │ └── reduce-similarities.md │ └── skills/ │ ├── byethrow/ │ │ └── SKILL.md │ └── use-gunshi-cli/ │ └── SKILL.md ├── .envrc ├── .githooks/ │ └── pre-commit ├── .github/ │ ├── FUNDING.yaml │ ├── actions/ │ │ └── setup-nix/ │ │ └── action.yaml │ ├── renovate.json │ └── workflows/ │ ├── check-pr-title.yaml │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .mcp.json ├── .oxfmtrc.jsonc ├── CLAUDE.md ├── apps/ │ ├── amp/ │ │ ├── CLAUDE.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── _consts.ts │ │ │ ├── _macro.ts │ │ │ ├── _types.ts │ │ │ ├── commands/ │ │ │ │ ├── daily.ts │ │ │ │ ├── index.ts │ │ │ │ ├── monthly.ts │ │ │ │ └── session.ts │ │ │ ├── data-loader.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ ├── pricing.ts │ │ │ └── run.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── ccusage/ │ │ ├── CLAUDE.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── config-schema.json │ │ ├── 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 │ │ │ ├── _macro.ts │ │ │ ├── _pricing-fetcher.ts │ │ │ ├── _project-names.ts │ │ │ ├── _session-blocks.ts │ │ │ ├── _shared-args.ts │ │ │ ├── _token-utils.ts │ │ │ ├── _types.ts │ │ │ ├── _utils.ts │ │ │ ├── calculate-cost.ts │ │ │ ├── commands/ │ │ │ │ ├── _session_id.ts │ │ │ │ ├── blocks.ts │ │ │ │ ├── daily.ts │ │ │ │ ├── index.ts │ │ │ │ ├── monthly.ts │ │ │ │ ├── session.ts │ │ │ │ ├── statusline.ts │ │ │ │ └── weekly.ts │ │ │ ├── data-loader.ts │ │ │ ├── debug.ts │ │ │ ├── index.ts │ │ │ └── logger.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 │ │ └── vitest.config.ts │ ├── codex/ │ │ ├── CLAUDE.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── _consts.ts │ │ │ ├── _macro.ts │ │ │ ├── _shared-args.ts │ │ │ ├── _types.ts │ │ │ ├── command-utils.ts │ │ │ ├── commands/ │ │ │ │ ├── daily.ts │ │ │ │ ├── monthly.ts │ │ │ │ └── session.ts │ │ │ ├── daily-report.ts │ │ │ ├── data-loader.ts │ │ │ ├── date-utils.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ ├── monthly-report.ts │ │ │ ├── pricing.ts │ │ │ ├── run.ts │ │ │ ├── session-report.ts │ │ │ └── token-utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── mcp/ │ │ ├── CLAUDE.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── ccusage.ts │ │ │ ├── cli-utils.ts │ │ │ ├── codex.ts │ │ │ ├── command.ts │ │ │ ├── consts.ts │ │ │ ├── index.ts │ │ │ ├── mcp-utils.ts │ │ │ └── mcp.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── opencode/ │ │ ├── CLAUDE.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── commands/ │ │ │ │ ├── daily.ts │ │ │ │ ├── index.ts │ │ │ │ ├── monthly.ts │ │ │ │ ├── session.ts │ │ │ │ └── weekly.ts │ │ │ ├── cost-utils.ts │ │ │ ├── data-loader.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ └── run.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── pi/ │ ├── CLAUDE.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src/ │ │ ├── _consts.ts │ │ ├── _pi-agent.ts │ │ ├── _types.ts │ │ ├── commands/ │ │ │ ├── daily.ts │ │ │ ├── index.ts │ │ │ ├── monthly.ts │ │ │ └── session.ts │ │ ├── data-loader.ts │ │ ├── index.ts │ │ └── logger.ts │ ├── tsconfig.json │ ├── tsdown.config.ts │ └── vitest.config.ts ├── ccusage.example.json ├── docs/ │ ├── .gitignore │ ├── .vitepress/ │ │ └── config.ts │ ├── CLAUDE.md │ ├── eslint.config.js │ ├── guide/ │ │ ├── blocks-reports.md │ │ ├── cli-options.md │ │ ├── codex/ │ │ │ ├── daily.md │ │ │ ├── index.md │ │ │ ├── monthly.md │ │ │ └── session.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 │ │ ├── opencode/ │ │ │ └── index.md │ │ ├── pi/ │ │ │ └── index.md │ │ ├── related-projects.md │ │ ├── session-reports.md │ │ ├── sponsors.md │ │ ├── statusline.md │ │ └── weekly-reports.md │ ├── index.md │ ├── package.json │ ├── public/ │ │ └── mcp-claude-desktop.avif │ ├── tsconfig.json │ ├── typedoc.config.ts │ ├── update-api-index.ts │ └── wrangler.jsonc ├── eslint.config.js ├── flake.nix ├── package.json ├── packages/ │ ├── internal/ │ │ ├── CLAUDE.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── constants.ts │ │ │ ├── format.ts │ │ │ ├── logger.ts │ │ │ ├── pricing-fetch-utils.ts │ │ │ └── pricing.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ └── terminal/ │ ├── CLAUDE.md │ ├── eslint.config.js │ ├── package.json │ ├── src/ │ │ ├── table.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── pnpm-workspace.yaml └── typos.toml
SYMBOL INDEX (472 symbols across 82 files)
FILE: apps/amp/src/_consts.ts
constant AMP_DATA_DIR_ENV (line 7) | const AMP_DATA_DIR_ENV = 'AMP_DATA_DIR';
constant DEFAULT_AMP_PATH (line 12) | const DEFAULT_AMP_PATH = '.local/share/amp';
constant USER_HOME_DIR (line 17) | const USER_HOME_DIR = homedir();
constant DEFAULT_AMP_DIR (line 22) | const DEFAULT_AMP_DIR = path.join(USER_HOME_DIR, DEFAULT_AMP_PATH);
constant AMP_THREADS_DIR_NAME (line 27) | const AMP_THREADS_DIR_NAME = 'threads';
constant AMP_THREAD_GLOB (line 32) | const AMP_THREAD_GLOB = '**/*.json';
constant MILLION (line 37) | const MILLION = 1_000_000;
FILE: apps/amp/src/_macro.ts
constant AMP_MODEL_PREFIXES (line 8) | const AMP_MODEL_PREFIXES = ['claude-', 'anthropic/'];
function isAmpModel (line 10) | function isAmpModel(modelName: string, _pricing: LiteLLMModelPricing): b...
function prefetchAmpPricing (line 14) | async function prefetchAmpPricing(): Promise<Record<string, LiteLLMModel...
FILE: apps/amp/src/_types.ts
type TokenUsageDelta (line 4) | type TokenUsageDelta = {
type TokenUsageEvent (line 15) | type TokenUsageEvent = TokenUsageDelta & {
type ModelUsage (line 26) | type ModelUsage = TokenUsageDelta & {
type DailyUsageSummary (line 33) | type DailyUsageSummary = {
type MonthlyUsageSummary (line 44) | type MonthlyUsageSummary = {
type SessionUsageSummary (line 55) | type SessionUsageSummary = {
type ModelPricing (line 68) | type ModelPricing = {
type PricingSource (line 78) | type PricingSource = {
type DailyReportRow (line 85) | type DailyReportRow = {
type MonthlyReportRow (line 100) | type MonthlyReportRow = {
type SessionReportRow (line 115) | type SessionReportRow = {
FILE: apps/amp/src/commands/daily.ts
constant TABLE_COLUMN_COUNT (line 15) | const TABLE_COLUMN_COUNT = 9;
function groupByDate (line 17) | function groupByDate(events: TokenUsageEvent[]): Map<string, TokenUsageE...
method run (line 45) | async run(ctx) {
FILE: apps/amp/src/commands/monthly.ts
constant TABLE_COLUMN_COUNT (line 15) | const TABLE_COLUMN_COUNT = 9;
function groupByMonth (line 17) | function groupByMonth(events: TokenUsageEvent[]): Map<string, TokenUsage...
method run (line 45) | async run(ctx) {
FILE: apps/amp/src/commands/session.ts
constant TABLE_COLUMN_COUNT (line 15) | const TABLE_COLUMN_COUNT = 9;
function groupByThread (line 17) | function groupByThread(events: TokenUsageEvent[]): Map<string, TokenUsag...
method run (line 44) | async run(ctx) {
FILE: apps/amp/src/data-loader.ts
type ParsedThread (line 82) | type ParsedThread = v.InferOutput<typeof threadSchema>;
type ParsedUsageLedgerEvent (line 83) | type ParsedUsageLedgerEvent = v.InferOutput<typeof usageLedgerEventSchema>;
type ParsedMessage (line 84) | type ParsedMessage = v.InferOutput<typeof messageSchema>;
function getAmpPath (line 90) | function getAmpPath(): string | null {
function findCacheTokensForEvent (line 111) | function findCacheTokensForEvent(
function convertLedgerEventToUsageEvent (line 136) | function convertLedgerEventToUsageEvent(
function loadThreadFile (line 167) | async function loadThreadFile(filePath: string): Promise<ParsedThread | ...
type LoadOptions (line 200) | type LoadOptions = {
type LoadResult (line 204) | type LoadResult = {
function loadAmpUsageEvents (line 213) | async function loadAmpUsageEvents(options: LoadOptions = {}): Promise<Lo...
FILE: apps/amp/src/pricing.ts
constant AMP_PROVIDER_PREFIXES (line 9) | const AMP_PROVIDER_PREFIXES = ['anthropic/'];
constant ZERO_MODEL_PRICING (line 10) | const ZERO_MODEL_PRICING = {
function toPerMillion (line 17) | function toPerMillion(value: number | undefined, fallback?: number): num...
type AmpPricingSourceOptions (line 22) | type AmpPricingSourceOptions = {
constant PREFETCHED_AMP_PRICING (line 27) | const PREFETCHED_AMP_PRICING = prefetchAmpPricing();
class AmpPricingSource (line 29) | class AmpPricingSource implements PricingSource, Disposable {
method constructor (line 32) | constructor(options: AmpPricingSourceOptions = {}) {
method getPricing (line 45) | async getPricing(model: string): Promise<ModelPricing> {
method calculateCost (line 71) | async calculateCost(
method [Symbol.dispose] (line 41) | [Symbol.dispose](): void {
FILE: apps/amp/src/run.ts
function run (line 14) | async function run(): Promise<void> {
FILE: apps/ccusage/scripts/generate-json-schema.ts
constant SCHEMA_FILENAME (line 26) | const SCHEMA_FILENAME = 'config-schema.json';
constant EXCLUDE_KEYS (line 32) | const EXCLUDE_KEYS = ['config'];
constant COMMAND_EXCLUDE_KEYS (line 38) | const COMMAND_EXCLUDE_KEYS: Record<string, string[]> = {
function tokensSchemaToJsonSchema (line 45) | function tokensSchemaToJsonSchema(schema: Record<string, any>): Record<s...
function createConfigSchemaJson (line 107) | function createConfigSchemaJson() {
function runFormat (line 191) | async function runFormat(files: string[]) {
function writeFile (line 198) | async function writeFile(path: string, content: string) {
function readFile (line 206) | async function readFile(path: string): Promise<Result.Result<string, any...
function copySchemaToDocsPublic (line 216) | async function copySchemaToDocsPublic() {
function generateJsonSchema (line 221) | async function generateJsonSchema() {
FILE: apps/ccusage/src/_config-loader-tokens.ts
type ConfigMergeContext (line 15) | type ConfigMergeContext<T extends Record<string, unknown>> = {
function extractExplicitArgs (line 29) | function extractExplicitArgs(tokens: unknown[]): Record<string, boolean> {
type ConfigData (line 45) | type ConfigData = {
function getConfigSearchPaths (line 57) | function getConfigSearchPaths(): string[] {
function validateConfigJson (line 65) | function validateConfigJson(data: unknown): data is ConfigData {
function loadConfigFile (line 102) | function loadConfigFile(filePath: string, debug = false): ConfigData | u...
function loadConfig (line 149) | function loadConfig(configPath?: string, debug = false): ConfigData | un...
function mergeConfigWithArgs (line 222) | function mergeConfigWithArgs<T extends Record<string, unknown>>(
function validateConfigFile (line 317) | function validateConfigFile(
FILE: apps/ccusage/src/_consts.ts
constant DEFAULT_RECENT_DAYS (line 9) | const DEFAULT_RECENT_DAYS = 3;
constant BLOCKS_WARNING_THRESHOLD (line 15) | const BLOCKS_WARNING_THRESHOLD = 0.8;
constant BLOCKS_COMPACT_WIDTH_THRESHOLD (line 21) | const BLOCKS_COMPACT_WIDTH_THRESHOLD = 120;
constant BLOCKS_DEFAULT_TERMINAL_WIDTH (line 27) | const BLOCKS_DEFAULT_TERMINAL_WIDTH = 120;
constant DEBUG_MATCH_THRESHOLD_PERCENT (line 33) | const DEBUG_MATCH_THRESHOLD_PERCENT = 0.1;
constant USER_HOME_DIR (line 39) | const USER_HOME_DIR = homedir();
constant XDG_CONFIG_DIR (line 45) | const XDG_CONFIG_DIR = xdgConfig ?? path.join(USER_HOME_DIR, '.config');
constant DEFAULT_CLAUDE_CODE_PATH (line 51) | const DEFAULT_CLAUDE_CODE_PATH = '.claude';
constant DEFAULT_CLAUDE_CONFIG_PATH (line 57) | const DEFAULT_CLAUDE_CONFIG_PATH = path.join(XDG_CONFIG_DIR, 'claude');
constant CLAUDE_CONFIG_DIR_ENV (line 63) | const CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR';
constant CLAUDE_PROJECTS_DIR_NAME (line 69) | const CLAUDE_PROJECTS_DIR_NAME = 'projects';
constant USAGE_DATA_GLOB_PATTERN (line 75) | const USAGE_DATA_GLOB_PATTERN = '**/*.jsonl';
constant MCP_DEFAULT_PORT (line 81) | const MCP_DEFAULT_PORT = 8080;
constant DEFAULT_REFRESH_INTERVAL_SECONDS (line 86) | const DEFAULT_REFRESH_INTERVAL_SECONDS = 1;
constant DEFAULT_CONTEXT_USAGE_THRESHOLDS (line 91) | const DEFAULT_CONTEXT_USAGE_THRESHOLDS = {
constant WEEK_DAYS (line 100) | const WEEK_DAYS = [
type WeekDay (line 113) | type WeekDay = (typeof WEEK_DAYS)[number];
type DayOfWeek (line 118) | type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6;
constant CONFIG_FILE_NAME (line 124) | const CONFIG_FILE_NAME = 'ccusage.json';
constant DEFAULT_LOCALE (line 130) | const DEFAULT_LOCALE = 'en-CA';
FILE: apps/ccusage/src/_daily-grouping.ts
type DailyData (line 9) | type DailyData = Awaited<ReturnType<typeof loadDailyUsageData>>;
function groupByProject (line 14) | function groupByProject(dailyData: DailyData): Record<string, DailyProje...
function groupDataByProject (line 43) | function groupDataByProject(dailyData: DailyData): Record<string, DailyD...
FILE: apps/ccusage/src/_date-utils.ts
type SortOrder (line 19) | type SortOrder = 'asc' | 'desc';
function createDateFormatter (line 27) | function createDateFormatter(timezone: string | undefined, locale: strin...
function formatDate (line 43) | function formatDate(dateStr: string, timezone?: string, locale?: string)...
function sortByDate (line 57) | function sortByDate<T>(
function filterByDateRange (line 81) | function filterByDateRange<T>(
function getDateWeek (line 109) | function getDateWeek(date: Date, startDay: DayOfWeek): WeeklyDate {
function getDayNumber (line 123) | function getDayNumber(day: WeekDay): DayOfWeek {
FILE: apps/ccusage/src/_jq-processor.ts
function processWithJq (line 10) | async function processWithJq(
FILE: apps/ccusage/src/_json-output-types.ts
type DailyProjectOutput (line 18) | type DailyProjectOutput = {
FILE: apps/ccusage/src/_macro.ts
function isClaudeModel (line 8) | function isClaudeModel(modelName: string, _pricing: LiteLLMModelPricing)...
function prefetchClaudePricing (line 16) | async function prefetchClaudePricing(): Promise<Record<string, LiteLLMMo...
FILE: apps/ccusage/src/_pricing-fetcher.ts
constant CLAUDE_PROVIDER_PREFIXES (line 6) | const CLAUDE_PROVIDER_PREFIXES = [
constant PREFETCHED_CLAUDE_PRICING (line 14) | const PREFETCHED_CLAUDE_PRICING = prefetchClaudePricing();
class PricingFetcher (line 16) | class PricingFetcher extends LiteLLMPricingFetcher {
method constructor (line 17) | constructor(offline = false) {
FILE: apps/ccusage/src/_project-names.ts
function parseProjectName (line 32) | function parseProjectName(projectName: string): string {
function formatProjectName (line 139) | function formatProjectName(projectName: string, aliases?: Map<string, st...
FILE: apps/ccusage/src/_session-blocks.ts
constant DEFAULT_SESSION_DURATION_HOURS (line 8) | const DEFAULT_SESSION_DURATION_HOURS = 5;
function floorToHour (line 15) | function floorToHour(timestamp: Date): Date {
type LoadedUsageEntry (line 24) | type LoadedUsageEntry = {
type TokenCounts (line 41) | type TokenCounts = {
type SessionBlock (line 51) | type SessionBlock = {
type BurnRate (line 68) | type BurnRate = {
type ProjectedUsage (line 77) | type ProjectedUsage = {
function identifySessionBlocks (line 90) | function identifySessionBlocks(
function createBlock (line 162) | function createBlock(
function createGapBlock (line 216) | function createGapBlock(
function calculateBurnRate (line 253) | function calculateBurnRate(block: SessionBlock): BurnRate | null {
function projectBlockUsage (line 295) | function projectBlockUsage(block: SessionBlock): ProjectedUsage | null {
function filterRecentBlocks (line 329) | function filterRecentBlocks(
function createMockEntry (line 345) | function createMockEntry(
FILE: apps/ccusage/src/_shared-args.ts
function parseDateArg (line 12) | function parseDateArg(value: string): string {
FILE: apps/ccusage/src/_token-utils.ts
type TokenCounts (line 12) | type TokenCounts = {
type AggregatedTokenCounts (line 22) | type AggregatedTokenCounts = {
type AnyTokenCounts (line 32) | type AnyTokenCounts = TokenCounts | AggregatedTokenCounts;
function getTotalTokens (line 40) | function getTotalTokens(tokenCounts: AnyTokenCounts): number {
FILE: apps/ccusage/src/_types.ts
type ModelName (line 91) | type ModelName = v.InferOutput<typeof modelNameSchema>;
type SessionId (line 92) | type SessionId = v.InferOutput<typeof sessionIdSchema>;
type RequestId (line 93) | type RequestId = v.InferOutput<typeof requestIdSchema>;
type MessageId (line 94) | type MessageId = v.InferOutput<typeof messageIdSchema>;
type ISOTimestamp (line 95) | type ISOTimestamp = v.InferOutput<typeof isoTimestampSchema>;
type DailyDate (line 96) | type DailyDate = v.InferOutput<typeof dailyDateSchema>;
type ActivityDate (line 97) | type ActivityDate = v.InferOutput<typeof activityDateSchema>;
type MonthlyDate (line 98) | type MonthlyDate = v.InferOutput<typeof monthlyDateSchema>;
type WeeklyDate (line 99) | type WeeklyDate = v.InferOutput<typeof weeklyDateSchema>;
type Bucket (line 100) | type Bucket = MonthlyDate | WeeklyDate;
type FilterDate (line 101) | type FilterDate = v.InferOutput<typeof filterDateSchema>;
type ProjectPath (line 102) | type ProjectPath = v.InferOutput<typeof projectPathSchema>;
type Version (line 103) | type Version = v.InferOutput<typeof versionSchema>;
function createISOTimestamp (line 113) | function createISOTimestamp(value: string): ISOTimestamp {
function createActivityDate (line 117) | function createActivityDate(value: string): ActivityDate {
function createBucket (line 126) | function createBucket(value: string): Bucket {
type CostMode (line 145) | type CostMode = TupleToUnion<typeof CostModes>;
type SortOrder (line 155) | type SortOrder = TupleToUnion<typeof SortOrders>;
type StatuslineHookJson (line 194) | type StatuslineHookJson = v.InferOutput<typeof statuslineHookJsonSchema>;
FILE: apps/ccusage/src/_utils.ts
function unreachable (line 5) | function unreachable(value: never): never {
function getFileModifiedTime (line 14) | async function getFileModifiedTime(filePath: string): Promise<number> {
FILE: apps/ccusage/src/calculate-cost.ts
type TokenData (line 27) | type TokenData = AggregatedTokenCounts;
type TokenTotals (line 32) | type TokenTotals = TokenData & {
type TotalsObject (line 39) | type TotalsObject = TokenTotals & {
function calculateTotals (line 48) | function calculateTotals(
function createTotalsObject (line 77) | function createTotalsObject(totals: TokenTotals): TotalsObject {
FILE: apps/ccusage/src/commands/_session_id.ts
type SessionIdContext (line 11) | type SessionIdContext = {
function handleSessionIdLookup (line 25) | async function handleSessionIdLookup(
function calculateSessionTotalTokens (line 103) | function calculateSessionTotalTokens(entries: UsageData[]): number {
FILE: apps/ccusage/src/commands/blocks.ts
function formatBlockTime (line 38) | function formatBlockTime(block: SessionBlock, compact = false, locale?: ...
function formatModels (line 97) | function formatModels(models: string[]): string {
function parseTokenLimit (line 111) | function parseTokenLimit(value: string | undefined, maxFromAll: number):...
method run (line 150) | async run(ctx) {
FILE: apps/ccusage/src/commands/daily.ts
method run (line 48) | async run(ctx) {
FILE: apps/ccusage/src/commands/index.ts
type CommandName (line 36) | type CommandName = (typeof subCommandUnion)[number][0];
function run (line 51) | async function run(): Promise<void> {
FILE: apps/ccusage/src/commands/monthly.ts
method run (line 26) | async run(ctx) {
FILE: apps/ccusage/src/commands/session.ts
method run (line 39) | async run(ctx): Promise<void> {
FILE: apps/ccusage/src/commands/statusline.ts
function formatRemainingTime (line 33) | function formatRemainingTime(remaining: number): string {
function getSemaphore (line 47) | function getSemaphore(
type SemaphoreType (line 64) | type SemaphoreType = {
function parseContextThreshold (line 101) | function parseContextThreshold(value: string): number {
method run (line 160) | async run(ctx) {
FILE: apps/ccusage/src/commands/weekly.ts
method run (line 36) | async run(ctx) {
FILE: apps/ccusage/src/data-loader.ts
function getClaudePaths (line 78) | function getClaudePaths(): string[] {
function extractProjectFromPath (line 150) | function extractProjectFromPath(jsonlPath: string): string {
type UsageData (line 220) | type UsageData = v.InferOutput<typeof usageDataSchema>;
type ModelBreakdown (line 237) | type ModelBreakdown = v.InferOutput<typeof modelBreakdownSchema>;
type DailyUsage (line 257) | type DailyUsage = v.InferOutput<typeof dailyUsageSchema>;
type SessionUsage (line 279) | type SessionUsage = v.InferOutput<typeof sessionUsageSchema>;
type MonthlyUsage (line 299) | type MonthlyUsage = v.InferOutput<typeof monthlyUsageSchema>;
type WeeklyUsage (line 319) | type WeeklyUsage = v.InferOutput<typeof weeklyUsageSchema>;
type BucketUsage (line 339) | type BucketUsage = v.InferOutput<typeof bucketUsageSchema>;
type TokenStats (line 344) | type TokenStats = {
function getDisplayModelName (line 352) | function getDisplayModelName(data: UsageData): string | undefined {
function aggregateByModel (line 363) | function aggregateByModel<T>(
function aggregateModelBreakdowns (line 405) | function aggregateModelBreakdowns(breakdowns: ModelBreakdown[]): Map<str...
function createModelBreakdowns (line 438) | function createModelBreakdowns(modelAggregates: Map<string, TokenStats>)...
function calculateTotals (line 450) | function calculateTotals<T>(
function filterByProject (line 483) | function filterByProject<T>(
function isDuplicateEntry (line 501) | function isDuplicateEntry(uniqueHash: string | null, processedHashes: Se...
function markAsProcessed (line 511) | function markAsProcessed(uniqueHash: string | null, processedHashes: Set...
function extractUniqueModels (line 520) | function extractUniqueModels<T>(
function createUniqueHash (line 530) | function createUniqueHash(data: UsageData): string | null {
function processJSONLFileByLine (line 547) | async function processJSONLFileByLine(
function getEarliestTimestamp (line 572) | async function getEarliestTimestamp(filePath: string): Promise<Date | nu...
function sortFilesByTimestamp (line 605) | async function sortFilesByTimestamp(files: string[]): Promise<string[]> {
function calculateCostForEntry (line 638) | async function calculateCostForEntry(
function getUsageLimitResetTime (line 685) | function getUsageLimitResetTime(data: UsageData): Date | null {
type GlobResult (line 706) | type GlobResult = {
function globUsageFiles (line 716) | async function globUsageFiles(claudePaths: string[]): Promise<GlobResult...
type DateFilter (line 733) | type DateFilter = {
type LoadOptions (line 741) | type LoadOptions = {
function loadDailyUsageData (line 760) | async function loadDailyUsageData(options?: LoadOptions): Promise<DailyU...
function loadSessionData (line 909) | async function loadSessionData(options?: LoadOptions): Promise<SessionUs...
function loadMonthlyUsageData (line 1092) | async function loadMonthlyUsageData(options?: LoadOptions): Promise<Mont...
function loadWeeklyUsageData (line 1104) | async function loadWeeklyUsageData(options?: LoadOptions): Promise<Weekl...
function loadSessionUsageById (line 1128) | async function loadSessionUsageById(
function loadBucketUsageData (line 1178) | async function loadBucketUsageData(
function calculateContextTokens (line 1264) | async function calculateContextTokens(
function loadSessionBlockData (line 1355) | async function loadSessionBlockData(options?: LoadOptions): Promise<Sess...
FILE: apps/ccusage/src/debug.ts
type Discrepancy (line 28) | type Discrepancy = {
type MismatchStats (line 47) | type MismatchStats = {
function detectMismatches (line 79) | async function detectMismatches(claudePath?: string): Promise<MismatchSt...
function printMismatchReport (line 220) | function printMismatchReport(stats: MismatchStats, sampleCount = 5): void {
FILE: apps/codex/src/_consts.ts
constant CODEX_HOME_ENV (line 4) | const CODEX_HOME_ENV = 'CODEX_HOME';
constant DEFAULT_CODEX_DIR (line 5) | const DEFAULT_CODEX_DIR = path.join(os.homedir(), '.codex');
constant DEFAULT_SESSION_SUBDIR (line 6) | const DEFAULT_SESSION_SUBDIR = 'sessions';
constant SESSION_GLOB (line 7) | const SESSION_GLOB = '**/*.jsonl';
constant DEFAULT_TIMEZONE (line 8) | const DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZon...
constant DEFAULT_LOCALE (line 9) | const DEFAULT_LOCALE = 'en-CA';
constant DEFAULT_PRECISION (line 10) | const DEFAULT_PRECISION = 2;
constant MILLION (line 12) | const MILLION = 1_000_000;
constant PRICING_CACHE_TTL_MS (line 14) | const PRICING_CACHE_TTL_MS = 1000 * 60 * 5;
FILE: apps/codex/src/_macro.ts
constant CODEX_MODEL_PREFIXES (line 8) | const CODEX_MODEL_PREFIXES = [
function isCodexModel (line 16) | function isCodexModel(modelName: string, _pricing: LiteLLMModelPricing):...
function prefetchCodexPricing (line 20) | async function prefetchCodexPricing(): Promise<Record<string, LiteLLMMod...
FILE: apps/codex/src/_types.ts
type TokenUsageDelta (line 1) | type TokenUsageDelta = {
type TokenUsageEvent (line 9) | type TokenUsageEvent = TokenUsageDelta & {
type ModelUsage (line 16) | type ModelUsage = TokenUsageDelta & {
type DailyUsageSummary (line 20) | type DailyUsageSummary = {
type MonthlyUsageSummary (line 27) | type MonthlyUsageSummary = {
type SessionUsageSummary (line 34) | type SessionUsageSummary = {
type ModelPricing (line 42) | type ModelPricing = {
type PricingLookupResult (line 48) | type PricingLookupResult = {
type PricingSource (line 53) | type PricingSource = {
type DailyReportRow (line 57) | type DailyReportRow = {
type MonthlyReportRow (line 68) | type MonthlyReportRow = {
type SessionReportRow (line 79) | type SessionReportRow = {
FILE: apps/codex/src/command-utils.ts
type UsageGroup (line 3) | type UsageGroup = {
function splitUsageTokens (line 10) | function splitUsageTokens(usage: UsageGroup): {
function formatModelsList (line 30) | function formatModelsList(
FILE: apps/codex/src/commands/daily.ts
constant TABLE_COLUMN_COUNT (line 21) | const TABLE_COLUMN_COUNT = 8;
method run (line 27) | async run(ctx) {
FILE: apps/codex/src/commands/monthly.ts
constant TABLE_COLUMN_COUNT (line 21) | const TABLE_COLUMN_COUNT = 8;
method run (line 27) | async run(ctx) {
FILE: apps/codex/src/commands/session.ts
constant TABLE_COLUMN_COUNT (line 26) | const TABLE_COLUMN_COUNT = 11;
method run (line 32) | async run(ctx) {
FILE: apps/codex/src/daily-report.ts
type DailyReportOptions (line 12) | type DailyReportOptions = {
function createSummary (line 20) | function createSummary(date: string, initialTimestamp: string): DailyUsa...
function buildDailyReport (line 34) | async function buildDailyReport(
method getPricing (line 138) | async getPricing(model: string): Promise<ModelPricing> {
FILE: apps/codex/src/data-loader.ts
type RawUsage (line 17) | type RawUsage = {
function ensureNumber (line 25) | function ensureNumber(value: unknown): number {
function normalizeRawUsage (line 42) | function normalizeRawUsage(value: unknown): RawUsage | null {
function subtractRawUsage (line 66) | function subtractRawUsage(current: RawUsage, previous: RawUsage | null):...
function convertToDelta (line 90) | function convertToDelta(raw: RawUsage): TokenUsageDelta {
constant LEGACY_FALLBACK_MODEL (line 105) | const LEGACY_FALLBACK_MODEL = 'gpt-5';
function extractModel (line 118) | function extractModel(value: unknown): string | undefined {
function asNonEmptyString (line 169) | function asNonEmptyString(value: unknown): string | undefined {
type LoadOptions (line 178) | type LoadOptions = {
type LoadResult (line 182) | type LoadResult = {
function loadTokenUsageEvents (line 187) | async function loadTokenUsageEvents(options: LoadOptions = {}): Promise<...
FILE: apps/codex/src/date-utils.ts
function safeTimeZone (line 1) | function safeTimeZone(timezone?: string): string {
function toDateKey (line 15) | function toDateKey(timestamp: string, timezone?: string): string {
function normalizeFilterDate (line 27) | function normalizeFilterDate(value?: string): string | undefined {
function isWithinRange (line 40) | function isWithinRange(dateKey: string, since?: string, until?: string):...
function formatDisplayDate (line 56) | function formatDisplayDate(dateKey: string, locale?: string, _timezone?:...
function toMonthKey (line 73) | function toMonthKey(timestamp: string, timezone?: string): string {
function formatDisplayMonth (line 85) | function formatDisplayMonth(monthKey: string, locale?: string, _timezone...
function formatDisplayDateTime (line 100) | function formatDisplayDateTime(
FILE: apps/codex/src/monthly-report.ts
type MonthlyReportOptions (line 12) | type MonthlyReportOptions = {
function createSummary (line 20) | function createSummary(month: string, initialTimestamp: string): Monthly...
function buildMonthlyReport (line 34) | async function buildMonthlyReport(
method getPricing (line 139) | async getPricing(model: string): Promise<ModelPricing> {
FILE: apps/codex/src/pricing.ts
constant CODEX_PROVIDER_PREFIXES (line 9) | const CODEX_PROVIDER_PREFIXES = ['openai/', 'azure/', 'openrouter/openai...
constant CODEX_MODEL_ALIASES_MAP (line 10) | const CODEX_MODEL_ALIASES_MAP = new Map<string, string>([
constant FREE_MODEL_PRICING (line 14) | const FREE_MODEL_PRICING = {
function isOpenRouterFreeModel (line 20) | function isOpenRouterFreeModel(model: string): boolean {
function hasNonZeroTokenPricing (line 29) | function hasNonZeroTokenPricing(pricing: LiteLLMModelPricing): boolean {
function toPerMillion (line 37) | function toPerMillion(value: number | undefined, fallback?: number): num...
type CodexPricingSourceOptions (line 42) | type CodexPricingSourceOptions = {
constant PREFETCHED_CODEX_PRICING (line 47) | const PREFETCHED_CODEX_PRICING = prefetchCodexPricing();
class CodexPricingSource (line 49) | class CodexPricingSource implements PricingSource, Disposable {
method constructor (line 52) | constructor(options: CodexPricingSourceOptions = {}) {
method getPricing (line 65) | async getPricing(model: string): Promise<ModelPricing> {
method [Symbol.dispose] (line 61) | [Symbol.dispose](): void {
FILE: apps/codex/src/run.ts
function run (line 16) | async function run(): Promise<void> {
FILE: apps/codex/src/session-report.ts
type SessionReportOptions (line 12) | type SessionReportOptions = {
function createSummary (line 20) | function createSummary(sessionId: string, initialTimestamp: string): Ses...
function buildSessionReport (line 35) | async function buildSessionReport(
method getPricing (line 167) | async getPricing(model: string): Promise<ModelPricing> {
FILE: apps/codex/src/token-utils.ts
function createEmptyUsage (line 5) | function createEmptyUsage(): TokenUsageDelta {
function addUsage (line 15) | function addUsage(target: TokenUsageDelta, delta: TokenUsageDelta): void {
function nonCachedInputTokens (line 23) | function nonCachedInputTokens(usage: TokenUsageDelta): number {
function calculateCostUSD (line 42) | function calculateCostUSD(usage: TokenUsageDelta, pricing: ModelPricing)...
FILE: apps/mcp/src/ccusage.ts
function getCcusageInvocation (line 22) | function getCcusageInvocation(): CliInvocation {
function runCcusageCliJson (line 32) | async function runCcusageCliJson(
function getCcusageDaily (line 67) | async function getCcusageDaily(
function getCcusageMonthly (line 85) | async function getCcusageMonthly(
function getCcusageSession (line 103) | async function getCcusageSession(
function getCcusageBlocks (line 121) | async function getCcusageBlocks(
FILE: apps/mcp/src/cli-utils.ts
type BinField (line 8) | type BinField = string | Record<string, string> | undefined;
type CliInvocation (line 10) | type CliInvocation = {
function resolveBinaryPath (line 18) | function resolveBinaryPath(packageName: string, binName?: string): string {
function createCliInvocation (line 56) | function createCliInvocation(entryPath: string): CliInvocation {
function executeCliCommand (line 74) | async function executeCliCommand(
FILE: apps/mcp/src/codex.ts
function getCodexInvocation (line 68) | function getCodexInvocation(): CliInvocation {
function runCodexCliJson (line 78) | async function runCodexCliJson(
function getCodexDaily (line 112) | async function getCodexDaily(parameters: z.infer<typeof codexParametersS...
function getCodexMonthly (line 117) | async function getCodexMonthly(parameters: z.infer<typeof codexParameter...
FILE: apps/mcp/src/command.ts
type McpType (line 10) | type McpType = (typeof MCP_TYPE_CHOICES)[number];
type Mode (line 11) | type Mode = LoadOptions['mode'];
constant MCP_DEFAULT_PORT (line 13) | const MCP_DEFAULT_PORT = 8080;
constant MODE_CHOICES (line 14) | const MODE_CHOICES = ['auto', 'calculate', 'display'] as const satisfies...
constant MCP_TYPE_CHOICES (line 15) | const MCP_TYPE_CHOICES = ['stdio', 'http'] as const satisfies readonly s...
type CommandOptions (line 17) | type CommandOptions = LoadOptions & {
method run (line 47) | async run(ctx) {
function run (line 88) | async function run(argv: string[] = process.argv.slice(2)): Promise<void> {
FILE: apps/mcp/src/consts.ts
constant DEFAULT_LOCALE (line 1) | const DEFAULT_LOCALE = 'en-CA';
constant DATE_FILTER_REGEX (line 2) | const DATE_FILTER_REGEX = /^\d{8}$/;
FILE: apps/mcp/src/mcp-utils.ts
function defaultOptions (line 4) | function defaultOptions(): LoadOptions {
FILE: apps/mcp/src/mcp.ts
function createMcpServer (line 46) | function createMcpServer(options?: LoadOptions): McpServer {
function startMcpServerStdio (line 192) | async function startMcpServerStdio(server: McpServer): Promise<void> {
function createMcpHttpApp (line 206) | function createMcpHttpApp(options?: LoadOptions): Hono {
FILE: apps/opencode/src/commands/daily.ts
constant TABLE_COLUMN_COUNT (line 17) | const TABLE_COLUMN_COUNT = 8;
method run (line 33) | async run(ctx) {
FILE: apps/opencode/src/commands/monthly.ts
constant TABLE_COLUMN_COUNT (line 17) | const TABLE_COLUMN_COUNT = 8;
method run (line 33) | async run(ctx) {
FILE: apps/opencode/src/commands/session.ts
constant TABLE_COLUMN_COUNT (line 17) | const TABLE_COLUMN_COUNT = 8;
method run (line 33) | async run(ctx) {
FILE: apps/opencode/src/commands/weekly.ts
constant TABLE_COLUMN_COUNT (line 17) | const TABLE_COLUMN_COUNT = 8;
function getISOWeek (line 25) | function getISOWeek(date: Date): string {
method run (line 58) | async run(ctx) {
FILE: apps/opencode/src/cost-utils.ts
constant MODEL_ALIASES (line 9) | const MODEL_ALIASES: Record<string, string> = {
function resolveModelName (line 14) | function resolveModelName(modelName: string): string {
function calculateCostForEntry (line 22) | async function calculateCostForEntry(
FILE: apps/opencode/src/data-loader.ts
constant DEFAULT_OPENCODE_PATH (line 22) | const DEFAULT_OPENCODE_PATH = '.local/share/opencode';
constant OPENCODE_STORAGE_DIR_NAME (line 27) | const OPENCODE_STORAGE_DIR_NAME = 'storage';
constant OPENCODE_MESSAGES_DIR_NAME (line 32) | const OPENCODE_MESSAGES_DIR_NAME = 'message';
constant OPENCODE_SESSIONS_DIR_NAME (line 33) | const OPENCODE_SESSIONS_DIR_NAME = 'session';
constant OPENCODE_CONFIG_DIR_ENV (line 38) | const OPENCODE_CONFIG_DIR_ENV = 'OPENCODE_DATA_DIR';
constant USER_HOME_DIR (line 43) | const USER_HOME_DIR = homedir();
type LoadedUsageEntry (line 105) | type LoadedUsageEntry = {
type LoadedSessionMetadata (line 118) | type LoadedSessionMetadata = {
function getOpenCodePath (line 130) | function getOpenCodePath(): string | null {
function loadOpenCodeMessage (line 154) | async function loadOpenCodeMessage(
function convertOpenCodeMessageToUsageEntry (line 171) | function convertOpenCodeMessageToUsageEntry(
function loadOpenCodeSession (line 190) | async function loadOpenCodeSession(
function convertOpenCodeSessionToMetadata (line 202) | function convertOpenCodeSessionToMetadata(
function loadOpenCodeSessions (line 214) | async function loadOpenCodeSessions(): Promise<Map<string, LoadedSession...
function loadOpenCodeMessages (line 255) | async function loadOpenCodeMessages(): Promise<LoadedUsageEntry[]> {
FILE: apps/opencode/src/run.ts
function run (line 15) | async function run(): Promise<void> {
FILE: apps/pi/src/_consts.ts
constant USER_HOME_DIR (line 4) | const USER_HOME_DIR = homedir();
constant PI_AGENT_DIR_ENV (line 6) | const PI_AGENT_DIR_ENV = 'PI_AGENT_DIR';
constant PI_AGENT_SESSIONS_DIR_NAME (line 7) | const PI_AGENT_SESSIONS_DIR_NAME = 'sessions';
constant DEFAULT_PI_AGENT_PATH (line 8) | const DEFAULT_PI_AGENT_PATH = path.join('.pi', 'agent');
FILE: apps/pi/src/_pi-agent.ts
type PiAgentMessage (line 36) | type PiAgentMessage = v.InferOutput<typeof piAgentMessageSchema>;
function isPiAgentUsageEntry (line 38) | function isPiAgentUsageEntry(data: PiAgentMessage): boolean {
function extractPiAgentSessionId (line 49) | function extractPiAgentSessionId(filePath: string): string {
function extractPiAgentProject (line 55) | function extractPiAgentProject(filePath: string): string {
function getPiAgentPaths (line 65) | function getPiAgentPaths(customPath?: string): string[] {
function transformPiAgentUsage (line 89) | function transformPiAgentUsage(data: PiAgentMessage): {
FILE: apps/pi/src/_types.ts
type ISOTimestamp (line 10) | type ISOTimestamp = v.InferOutput<typeof isoTimestampSchema>;
FILE: apps/pi/src/commands/daily.ts
method run (line 52) | async run(ctx) {
FILE: apps/pi/src/commands/monthly.ts
method run (line 52) | async run(ctx) {
FILE: apps/pi/src/commands/session.ts
method run (line 53) | async run(ctx) {
FILE: apps/pi/src/data-loader.ts
type Source (line 13) | type Source = 'claude-code' | 'pi-agent';
type LoadOptions (line 15) | type LoadOptions = {
type DailyUsageWithSource (line 23) | type DailyUsageWithSource = {
type SessionUsageWithSource (line 42) | type SessionUsageWithSource = {
type MonthlyUsageWithSource (line 63) | type MonthlyUsageWithSource = {
function processJSONLFileByLine (line 82) | async function processJSONLFileByLine(
function globPiAgentFiles (line 100) | async function globPiAgentFiles(paths: string[]): Promise<string[]> {
function formatDate (line 112) | function formatDate(timestamp: string, timezone?: string): string {
function formatMonth (line 118) | function formatMonth(timestamp: string, timezone?: string): string {
function normalizeDate (line 125) | function normalizeDate(value: string): string {
function isInDateRange (line 129) | function isInDateRange(date: string, since?: string, until?: string): bo...
type EntryData (line 140) | type EntryData = {
function loadPiAgentData (line 152) | async function loadPiAgentData(options?: LoadOptions): Promise<EntryData...
function aggregateByModel (line 210) | function aggregateByModel(entries: EntryData[]): Map<
function createBreakdowns (line 253) | function createBreakdowns(
function calculateTotals (line 278) | function calculateTotals(entries: EntryData[]): {
function loadPiAgentDailyData (line 302) | async function loadPiAgentDailyData(options?: LoadOptions): Promise<Dail...
function loadPiAgentSessionData (line 341) | async function loadPiAgentSessionData(
function loadPiAgentMonthlyData (line 392) | async function loadPiAgentMonthlyData(
FILE: apps/pi/src/index.ts
function run (line 18) | async function run(): Promise<void> {
FILE: docs/.vitepress/config.ts
method config (line 180) | config(md) {
FILE: docs/typedoc.config.ts
type TypedocConfig (line 5) | type TypedocConfig = TypeDocOptions & PluginOptions & { docsRoot?: strin...
FILE: docs/update-api-index.ts
function updateApiIndex (line 22) | async function updateApiIndex() {
function updateConstsPage (line 51) | async function updateConstsPage() {
FILE: packages/internal/src/constants.ts
constant DEFAULT_LOCALE (line 5) | const DEFAULT_LOCALE = 'en-CA';
constant MILLION (line 11) | const MILLION = 1_000_000;
FILE: packages/internal/src/format.ts
function formatTokens (line 6) | function formatTokens(value: number): string {
function formatCurrency (line 16) | function formatCurrency(value: number, locale?: string): string {
FILE: packages/internal/src/logger.ts
function createLogger (line 5) | function createLogger(name: string): ConsolaInstance {
FILE: packages/internal/src/pricing-fetch-utils.ts
type PricingDataset (line 5) | type PricingDataset = Record<string, LiteLLMModelPricing>;
function createPricingDataset (line 7) | function createPricingDataset(): PricingDataset {
function fetchLiteLLMPricingDataset (line 11) | async function fetchLiteLLMPricingDataset(): Promise<PricingDataset> {
function filterPricingDataset (line 36) | function filterPricingDataset(
FILE: packages/internal/src/pricing.ts
constant LITELLM_PRICING_URL (line 4) | const LITELLM_PRICING_URL =
constant DEFAULT_TIERED_THRESHOLD (line 14) | const DEFAULT_TIERED_THRESHOLD = 200_000;
type LiteLLMModelPricing (line 55) | type LiteLLMModelPricing = v.InferOutput<typeof liteLLMModelPricingSchema>;
type PricingLogger (line 57) | type PricingLogger = {
type LiteLLMPricingFetcherOptions (line 64) | type LiteLLMPricingFetcherOptions = {
constant DEFAULT_PROVIDER_PREFIXES (line 72) | const DEFAULT_PROVIDER_PREFIXES = [
function createLogger (line 82) | function createLogger(logger?: PricingLogger): PricingLogger {
class LiteLLMPricingFetcher (line 95) | class LiteLLMPricingFetcher implements Disposable {
method constructor (line 103) | constructor(options: LiteLLMPricingFetcherOptions = {}) {
method clearCache (line 115) | clearCache(): void {
method handleFallbackToCachedPricing (line 132) | private async handleFallbackToCachedPricing(
method ensurePricingLoaded (line 151) | private async ensurePricingLoaded(): Result.ResultAsync<Map<string, Li...
method fetchModelPricing (line 206) | async fetchModelPricing(): Result.ResultAsync<Map<string, LiteLLMModel...
method createMatchingCandidates (line 210) | private createMatchingCandidates(modelName: string): string[] {
method getModelPricing (line 221) | async getModelPricing(modelName: string): Result.ResultAsync<LiteLLMMo...
method getModelContextLimit (line 245) | async getModelContextLimit(modelName: string): Result.ResultAsync<numb...
method calculateCostFromPricing (line 267) | calculateCostFromPricing(
method calculateCostFromTokens (line 345) | async calculateCostFromTokens(
method [Symbol.dispose] (line 111) | [Symbol.dispose](): void {
FILE: packages/terminal/src/table.ts
constant DEFAULT_LOCALE (line 11) | const DEFAULT_LOCALE = 'en-CA';
function createDatePartsFormatter (line 19) | function createDatePartsFormatter(
function formatDateCompact (line 38) | function formatDateCompact(dateStr: string, timezone?: string, locale?: ...
type TableCellAlign (line 59) | type TableCellAlign = 'left' | 'right' | 'center';
type TableRow (line 64) | type TableRow = (string | number | { content: string; hAlign?: TableCell...
type TableOptions (line 69) | type TableOptions = {
class ResponsiveTable (line 87) | class ResponsiveTable {
method constructor (line 104) | constructor(options: TableOptions) {
method push (line 120) | push(row: TableRow): void {
method filterRowToCompact (line 130) | private filterRowToCompact(row: TableRow, compactIndices: number[]): T...
method getCurrentTableConfig (line 138) | private getCurrentTableConfig(): { head: string[]; colAligns: TableCel...
method getCompactIndices (line 149) | private getCompactIndices(): number[] {
method isCompactMode (line 172) | isCompactMode(): boolean {
method toString (line 181) | toString(): string {
method isSeparatorRow (line 333) | private isSeparatorRow(row: TableRow): boolean {
method isDateString (line 348) | private isDateString(text: string): boolean {
function formatNumber (line 359) | function formatNumber(num: number): string {
function formatCurrency (line 368) | function formatCurrency(amount: number): string {
function formatModelName (line 378) | function formatModelName(modelName: string): string {
function formatModelsDisplay (line 416) | function formatModelsDisplay(models: string[]): string {
function formatModelsDisplayMultiline (line 428) | function formatModelsDisplayMultiline(models: string[]): string {
function pushBreakdownRows (line 445) | function pushBreakdownRows(
type UsageReportConfig (line 494) | type UsageReportConfig = {
type UsageData (line 508) | type UsageData = {
function createUsageReportTable (line 522) | function createUsageReportTable(config: UsageReportConfig): ResponsiveTa...
function formatUsageDataRow (line 576) | function formatUsageDataRow(
function formatTotalsRow (line 608) | function formatTotalsRow(
function addEmptySeparatorRow (line 638) | function addEmptySeparatorRow(table: ResponsiveTable, columnCount: numbe...
FILE: packages/terminal/src/utils.ts
constant SYNC_START (line 9) | const SYNC_START = '\x1B[?2026h';
constant SYNC_END (line 10) | const SYNC_END = '\x1B[?2026l';
constant DISABLE_LINE_WRAP (line 13) | const DISABLE_LINE_WRAP = '\x1B[?7l';
constant ENABLE_LINE_WRAP (line 14) | const ENABLE_LINE_WRAP = '\x1B[?7h';
constant ANSI_RESET (line 17) | const ANSI_RESET = '\u001B[0m';
class TerminalManager (line 24) | class TerminalManager {
method constructor (line 32) | constructor(stream: WriteStream = process.stdout) {
method hideCursor (line 40) | hideCursor(): void {
method showCursor (line 52) | showCursor(): void {
method clearScreen (line 63) | clearScreen(): void {
method write (line 75) | write(text: string): void {
method startBuffering (line 87) | startBuffering(): void {
method flush (line 96) | flush(): void {
method enterAlternateScreen (line 113) | enterAlternateScreen(): void {
method exitAlternateScreen (line 125) | exitAlternateScreen(): void {
method enableSyncMode (line 138) | enableSyncMode(): void {
method disableSyncMode (line 145) | disableSyncMode(): void {
method width (line 153) | get width(): number {
method height (line 161) | get height(): number {
method isTTY (line 169) | get isTTY(): boolean {
method cleanup (line 177) | cleanup(): void {
function createProgressBar (line 206) | function createProgressBar(
function centerText (line 286) | function centerText(text: string, width: number): string {
constant SAVE_CURSOR (line 307) | const SAVE_CURSOR = '\u001B7';
constant RESTORE_CURSOR (line 308) | const RESTORE_CURSOR = '\u001B8';
function drawEmoji (line 314) | function drawEmoji(emoji: string): string {
Condensed preview — 211 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,071K chars).
[
{
"path": ".claude/commands/reduce-similarities.md",
"chars": 197,
"preview": "Run `similarity-ts .` to detect semantic code similarities. Execute this command, analyze the duplicate code patterns, a"
},
{
"path": ".claude/skills/byethrow/SKILL.md",
"chars": 1799,
"preview": "---\nname: byethrow\ndescription: Reference the byethrow documentation to understand and use the Result type library for e"
},
{
"path": ".claude/skills/use-gunshi-cli/SKILL.md",
"chars": 431,
"preview": "---\nname: use-gunshi-cli\ndescription: Use the Gunshi library to create command-line interfaces in JavaScript/TypeScript."
},
{
"path": ".envrc",
"chars": 67,
"preview": "watch_file pnpm-lock.yaml\nwatch_file pnpm-workspace.yaml\nuse flake\n"
},
{
"path": ".githooks/pre-commit",
"chars": 58,
"preview": "#!/bin/sh\n\n# Run lint-staged\nnpx --no-install lint-staged\n"
},
{
"path": ".github/FUNDING.yaml",
"chars": 18,
"preview": "github: ryoppippi\n"
},
{
"path": ".github/actions/setup-nix/action.yaml",
"chars": 884,
"preview": "name: Setup Nix\ndescription: Install Nix and configure Cachix\ninputs:\n cachix-auth-token:\n description: Cachix authe"
},
{
"path": ".github/renovate.json",
"chars": 159,
"preview": "{\n\t\"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n\t\"nix\": {\n\t\t\"enabled\": true\n\t},\n\t\"extends\": [\"github>"
},
{
"path": ".github/workflows/check-pr-title.yaml",
"chars": 498,
"preview": "name: Check PR title\n\non:\n pull_request:\n types:\n - opened\n - reopened\n - edited\n - synchronize\n"
},
{
"path": ".github/workflows/ci.yaml",
"chars": 2214,
"preview": "name: CI\n\non:\n push:\n pull_request:\n\njobs:\n lint-check:\n runs-on: ubuntu-24.04-arm\n\n steps:\n - uses: actio"
},
{
"path": ".github/workflows/release.yaml",
"chars": 1043,
"preview": "name: npm publish\n\non:\n push:\n tags:\n - '*'\n\njobs:\n npm:\n runs-on: ubuntu-24.04-arm\n timeout-minutes: 10"
},
{
"path": ".gitignore",
"chars": 425,
"preview": "# dependencies (bun install)\nnode_modules\n\n# output\nout\ndist\n*.tgz\n\n# code coverage\ncoverage\n*.lcov\n\n# logs\nlogs\n_.log\nr"
},
{
"path": ".mcp.json",
"chars": 173,
"preview": "{\n\t\"mcpServers\": {\n\t\t\"context7\": {\n\t\t\t\"type\": \"http\",\n\t\t\t\"url\": \"https://mcp.context7.com/mcp\"\n\t\t},\n\t\t\"grep\": {\n\t\t\t\"type"
},
{
"path": ".oxfmtrc.jsonc",
"chars": 210,
"preview": "{\n\t\"$schema\": \"https://unpkg.com/oxfmt/configuration_schema.json\",\n\t\"useTabs\": true,\n\t\"singleQuote\": true,\n\t\"files\": {\n\t"
},
{
"path": "CLAUDE.md",
"chars": 15627,
"preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
},
{
"path": "apps/amp/CLAUDE.md",
"chars": 3593,
"preview": "# Amp CLI Notes\n\n## Log Sources\n\n- Amp session usage is recorded under `${AMP_DATA_DIR:-~/.local/share/amp}/threads/` (t"
},
{
"path": "apps/amp/eslint.config.js",
"chars": 273,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\n/** @type {import('eslint').Linter.FlatConfig[]} */\nconst config "
},
{
"path": "apps/amp/package.json",
"chars": 1724,
"preview": "{\n\t\"name\": \"@ccusage/amp\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"description\": \"Usage analysis tool for Amp CLI se"
},
{
"path": "apps/amp/src/_consts.ts",
"chars": 753,
"preview": "import { homedir } from 'node:os';\nimport path from 'node:path';\n\n/**\n * Environment variable name for custom Amp data d"
},
{
"path": "apps/amp/src/_macro.ts",
"chars": 770,
"preview": "import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';\nimport {\n\tcreatePricingDataset,\n\tfetchLiteLLMPrici"
},
{
"path": "apps/amp/src/_types.ts",
"chars": 2472,
"preview": "/**\n * Token usage delta for a single event\n */\nexport type TokenUsageDelta = {\n\tinputTokens: number;\n\tcacheCreationInpu"
},
{
"path": "apps/amp/src/commands/daily.ts",
"chars": 5561,
"preview": "import type { TokenUsageEvent } from '../_types.ts';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tformatDateCompact"
},
{
"path": "apps/amp/src/commands/index.ts",
"chars": 137,
"preview": "export { dailyCommand } from './daily.ts';\nexport { monthlyCommand } from './monthly.ts';\nexport { sessionCommand } from"
},
{
"path": "apps/amp/src/commands/monthly.ts",
"chars": 5624,
"preview": "import type { TokenUsageEvent } from '../_types.ts';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tformatDateCompact"
},
{
"path": "apps/amp/src/commands/session.ts",
"chars": 6067,
"preview": "import type { TokenUsageEvent } from '../_types.ts';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tformatDateCompact"
},
{
"path": "apps/amp/src/data-loader.ts",
"chars": 9510,
"preview": "/**\n * @fileoverview Data loading utilities for Amp CLI usage analysis\n *\n * This module provides functions for loading "
},
{
"path": "apps/amp/src/index.ts",
"chars": 120,
"preview": "#!/usr/bin/env node\n\nimport { run } from './run.ts';\n\n// eslint-disable-next-line antfu/no-top-level-await\nawait run();\n"
},
{
"path": "apps/amp/src/logger.ts",
"chars": 110,
"preview": "import { createLogger } from '@ccusage/internal/logger';\n\nexport const logger = createLogger('@ccusage/amp');\n"
},
{
"path": "apps/amp/src/pricing.ts",
"chars": 4612,
"preview": "import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';\nimport type { ModelPricing, PricingSource } from '"
},
{
"path": "apps/amp/src/run.ts",
"chars": 762,
"preview": "import process from 'node:process';\nimport { cli } from 'gunshi';\nimport { description, name, version } from '../package"
},
{
"path": "apps/amp/tsconfig.json",
"chars": 650,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"lib\": [\"ESNext\"],\n\t\t\"moduleDetection\": \"force\",\n\t\t\"module\": \"Preserve\","
},
{
"path": "apps/amp/tsdown.config.ts",
"chars": 221,
"preview": "import { defineConfig } from 'tsdown';\n\nexport default defineConfig({\n\tentry: ['src/index.ts'],\n\tformat: ['esm'],\n\tclean"
},
{
"path": "apps/amp/vitest.config.ts",
"chars": 274,
"preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n\ttest: {\n\t\tglobals: true,\n\t\tincludeSource: "
},
{
"path": "apps/ccusage/CLAUDE.md",
"chars": 4546,
"preview": "# CLAUDE.md - ccusage Package\n\nThis is the main ccusage CLI package that provides usage analysis for Claude Code.\n\n## Pa"
},
{
"path": "apps/ccusage/LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2025 ryoppippi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "apps/ccusage/README.md",
"chars": 10510,
"preview": "<div align=\"center\">\n <img src=\"https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/logo.svg\" alt=\"ccusage"
},
{
"path": "apps/ccusage/config-schema.json",
"chars": 30217,
"preview": "{\n\t\"$ref\": \"#/definitions/ccusage-config\",\n\t\"definitions\": {\n\t\t\"ccusage-config\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"properties\":"
},
{
"path": "apps/ccusage/eslint.config.js",
"chars": 273,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\n/** @type {import('eslint').Linter.FlatConfig[]} */\nconst config "
},
{
"path": "apps/ccusage/package.json",
"chars": 3632,
"preview": "{\n\t\"name\": \"ccusage\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"description\": \"Usage analysis tool for Claude Code\",\n\t"
},
{
"path": "apps/ccusage/scripts/generate-json-schema.ts",
"chars": 10725,
"preview": "#!/usr/bin/env bun\n\n/**\n * @fileoverview Generate JSON Schema from args-tokens configuration schema\n *\n * This script ge"
},
{
"path": "apps/ccusage/src/_config-loader-tokens.ts",
"chars": 27430,
"preview": "import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport process from 'node:process'"
},
{
"path": "apps/ccusage/src/_consts.ts",
"chars": 3618,
"preview": "import { homedir } from 'node:os';\nimport path from 'node:path';\nimport { xdgConfig } from 'xdg-basedir';\n\n/**\n * Defaul"
},
{
"path": "apps/ccusage/src/_daily-grouping.ts",
"chars": 4084,
"preview": "import type { DailyProjectOutput } from './_json-output-types.ts';\nimport type { loadDailyUsageData } from './data-loade"
},
{
"path": "apps/ccusage/src/_date-utils.ts",
"chars": 8960,
"preview": "/**\n * Date utility functions for handling date formatting, filtering, and manipulation\n * @module date-utils\n */\n\nimpor"
},
{
"path": "apps/ccusage/src/_jq-processor.ts",
"chars": 3065,
"preview": "import { Result } from '@praha/byethrow';\nimport spawn from 'nano-spawn';\n\n/**\n * Process JSON data with a jq command\n *"
},
{
"path": "apps/ccusage/src/_json-output-types.ts",
"chars": 830,
"preview": "/**\n * @fileoverview JSON output interface types for daily command groupByProject function\n *\n * This module provides Ty"
},
{
"path": "apps/ccusage/src/_macro.ts",
"chars": 794,
"preview": "import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';\nimport {\n\tcreatePricingDataset,\n\tfetchLiteLLMPrici"
},
{
"path": "apps/ccusage/src/_pricing-fetcher.ts",
"chars": 1367,
"preview": "import { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';\nimport { Result } from '@praha/byethrow';\nimport { pr"
},
{
"path": "apps/ccusage/src/_project-names.ts",
"chars": 7337,
"preview": "/**\n * @fileoverview Project name formatting and alias utilities\n *\n * Provides utilities for formatting raw project dir"
},
{
"path": "apps/ccusage/src/_session-blocks.ts",
"chars": 33474,
"preview": "import { uniq } from 'es-toolkit';\nimport { DEFAULT_RECENT_DAYS } from './_consts.ts';\nimport { getTotalTokens } from '."
},
{
"path": "apps/ccusage/src/_shared-args.ts",
"chars": 3167,
"preview": "import type { Args } from 'gunshi';\nimport type { CostMode, SortOrder } from './_types.ts';\nimport * as v from 'valibot'"
},
{
"path": "apps/ccusage/src/_token-utils.ts",
"chars": 3216,
"preview": "/**\n * @fileoverview Token calculation utilities\n *\n * This module provides shared utilities for calculating token total"
},
{
"path": "apps/ccusage/src/_types.ts",
"chars": 6230,
"preview": "import type { TupleToUnion } from 'type-fest';\nimport * as v from 'valibot';\n\n/**\n * Branded Valibot schemas for type sa"
},
{
"path": "apps/ccusage/src/_utils.ts",
"chars": 2523,
"preview": "import { stat, utimes, writeFile } from 'node:fs/promises';\nimport { Result } from '@praha/byethrow';\nimport { createFix"
},
{
"path": "apps/ccusage/src/calculate-cost.ts",
"chars": 5822,
"preview": "/**\n * @fileoverview Cost calculation utilities for usage data analysis\n *\n * This module provides functions for calcula"
},
{
"path": "apps/ccusage/src/commands/_session_id.ts",
"chars": 3528,
"preview": "import type { CostMode } from '../_types.ts';\nimport type { UsageData } from '../data-loader.ts';\nimport process from 'n"
},
{
"path": "apps/ccusage/src/commands/blocks.ts",
"chars": 15657,
"preview": "import type { SessionBlock } from '../_session-blocks.ts';\nimport process from 'node:process';\nimport {\n\tformatCurrency,"
},
{
"path": "apps/ccusage/src/commands/daily.ts",
"chars": 7424,
"preview": "import type { UsageReportConfig } from '@ccusage/terminal/table';\nimport process from 'node:process';\nimport {\n\taddEmpty"
},
{
"path": "apps/ccusage/src/commands/index.ts",
"chars": 1620,
"preview": "import process from 'node:process';\nimport { cli } from 'gunshi';\nimport { description, name, version } from '../../pack"
},
{
"path": "apps/ccusage/src/commands/monthly.ts",
"chars": 4647,
"preview": "import type { UsageReportConfig } from '@ccusage/terminal/table';\nimport process from 'node:process';\nimport {\n\taddEmpty"
},
{
"path": "apps/ccusage/src/commands/session.ts",
"chars": 5955,
"preview": "import type { UsageReportConfig } from '@ccusage/terminal/table';\nimport process from 'node:process';\nimport {\n\taddEmpty"
},
{
"path": "apps/ccusage/src/commands/statusline.ts",
"chars": 19558,
"preview": "import type { Formatter } from 'picocolors/types';\nimport { mkdirSync } from 'node:fs';\nimport { tmpdir } from 'node:os'"
},
{
"path": "apps/ccusage/src/commands/weekly.ts",
"chars": 4757,
"preview": "import type { UsageReportConfig } from '@ccusage/terminal/table';\nimport process from 'node:process';\nimport {\n\taddEmpty"
},
{
"path": "apps/ccusage/src/data-loader.ts",
"chars": 145343,
"preview": "/**\n * @fileoverview Data loading utilities for Claude Code usage analysis\n *\n * This module provides functions for load"
},
{
"path": "apps/ccusage/src/debug.ts",
"chars": 17879,
"preview": "/**\n * @fileoverview Debug utilities for cost calculation validation\n *\n * This module provides debugging tools for dete"
},
{
"path": "apps/ccusage/src/index.ts",
"chars": 363,
"preview": "#!/usr/bin/env node\n\n/**\n * @fileoverview Main entry point for ccusage CLI tool\n *\n * This is the main entry point for t"
},
{
"path": "apps/ccusage/src/logger.ts",
"chars": 585,
"preview": "/**\n * @fileoverview Logging utilities for the ccusage application\n *\n * This module provides configured logger instance"
},
{
"path": "apps/ccusage/test/statusline-test-opus4.json",
"chars": 677,
"preview": "{\n\t\"session_id\": \"test-session-opus4\",\n\t\"transcript_path\": \"test/test-transcript.jsonl\",\n\t\"cwd\": \"/Users/test/project\",\n"
},
{
"path": "apps/ccusage/test/statusline-test-sonnet4.json",
"chars": 678,
"preview": "{\n\t\"session_id\": \"test-session-sonnet4\",\n\t\"transcript_path\": \"test/test-transcript.jsonl\",\n\t\"cwd\": \"/Users/test/project\""
},
{
"path": "apps/ccusage/test/statusline-test-sonnet41.json",
"chars": 683,
"preview": "{\n\t\"session_id\": \"test-session-sonnet41\",\n\t\"transcript_path\": \"test/test-transcript.jsonl\",\n\t\"cwd\": \"/Users/test/project"
},
{
"path": "apps/ccusage/test/statusline-test.json",
"chars": 906,
"preview": "{\n\t\"session_id\": \"73cc9f9a-2775-4418-beec-bc36b62a1c6f\",\n\t\"transcript_path\": \"/Users/ryoppippi/.config/claude/projects/-"
},
{
"path": "apps/ccusage/test/test-transcript.jsonl",
"chars": 350,
"preview": "{\"type\":\"user\",\"message\":{}}\n{\"type\":\"assistant\",\"message\":{\"usage\":{\"input_tokens\":1000,\"output_tokens\":50,\"cache_creat"
},
{
"path": "apps/ccusage/tsconfig.json",
"chars": 811,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"jsx\": \"react-jsx\",\n\t\t// Environment setup & latest features\n\t\t\"lib\": [\""
},
{
"path": "apps/ccusage/tsdown.config.ts",
"chars": 730,
"preview": "import { defineConfig } from 'tsdown';\nimport Macros from 'unplugin-macros/rolldown';\n\nexport default defineConfig({\n\ten"
},
{
"path": "apps/ccusage/vitest.config.ts",
"chars": 371,
"preview": "import Macros from 'unplugin-macros/vite';\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n"
},
{
"path": "apps/codex/CLAUDE.md",
"chars": 4825,
"preview": "# Codex CLI Notes\n\n## Log Sources\n\n- Codex session usage is recorded under `${CODEX_HOME:-~/.codex}/sessions/` (the CLI "
},
{
"path": "apps/codex/README.md",
"chars": 6477,
"preview": "<div align=\"center\">\n <img src=\"https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/logo.svg\" alt=\"ccusage"
},
{
"path": "apps/codex/eslint.config.js",
"chars": 273,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\n/** @type {import('eslint').Linter.FlatConfig[]} */\nconst config "
},
{
"path": "apps/codex/package.json",
"chars": 1703,
"preview": "{\n\t\"name\": \"@ccusage/codex\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"description\": \"Usage analysis tool for OpenAI C"
},
{
"path": "apps/codex/src/_consts.ts",
"chars": 527,
"preview": "import os from 'node:os';\nimport path from 'node:path';\n\nexport const CODEX_HOME_ENV = 'CODEX_HOME';\nexport const DEFAUL"
},
{
"path": "apps/codex/src/_macro.ts",
"chars": 842,
"preview": "import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';\nimport {\n\tcreatePricingDataset,\n\tfetchLiteLLMPrici"
},
{
"path": "apps/codex/src/_shared-args.ts",
"chars": 1347,
"preview": "import type { Args } from 'gunshi';\nimport { DEFAULT_LOCALE, DEFAULT_TIMEZONE } from './_consts.ts';\n\nexport const share"
},
{
"path": "apps/codex/src/_types.ts",
"chars": 1918,
"preview": "export type TokenUsageDelta = {\n\tinputTokens: number;\n\tcachedInputTokens: number;\n\toutputTokens: number;\n\treasoningOutpu"
},
{
"path": "apps/codex/src/command-utils.ts",
"chars": 1024,
"preview": "import { sort } from 'fast-sort';\n\nexport type UsageGroup = {\n\tinputTokens: number;\n\tcachedInputTokens: number;\n\toutputT"
},
{
"path": "apps/codex/src/commands/daily.ts",
"chars": 5020,
"preview": "import process from 'node:process';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tformatDateCompact,\n\tformatModelsDi"
},
{
"path": "apps/codex/src/commands/monthly.ts",
"chars": 5053,
"preview": "import process from 'node:process';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tformatDateCompact,\n\tformatModelsDi"
},
{
"path": "apps/codex/src/commands/session.ts",
"chars": 5845,
"preview": "import process from 'node:process';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tformatDateCompact,\n\tformatModelsDi"
},
{
"path": "apps/codex/src/daily-report.ts",
"chars": 5522,
"preview": "import type {\n\tDailyReportRow,\n\tDailyUsageSummary,\n\tModelPricing,\n\tModelUsage,\n\tPricingSource,\n\tTokenUsageEvent,\n} from "
},
{
"path": "apps/codex/src/data-loader.ts",
"chars": 13883,
"preview": "import type { TokenUsageDelta, TokenUsageEvent } from './_types.ts';\nimport { readFile, stat } from 'node:fs/promises';\n"
},
{
"path": "apps/codex/src/date-utils.ts",
"chars": 3402,
"preview": "function safeTimeZone(timezone?: string): string {\n\tif (timezone == null || timezone.trim() === '') {\n\t\treturn Intl.Date"
},
{
"path": "apps/codex/src/index.ts",
"chars": 120,
"preview": "#!/usr/bin/env node\n\nimport { run } from './run.ts';\n\n// eslint-disable-next-line antfu/no-top-level-await\nawait run();\n"
},
{
"path": "apps/codex/src/logger.ts",
"chars": 194,
"preview": "import { createLogger, log as internalLog } from '@ccusage/internal/logger';\n\nimport { name } from '../package.json';\n\ne"
},
{
"path": "apps/codex/src/monthly-report.ts",
"chars": 5587,
"preview": "import type {\n\tModelPricing,\n\tModelUsage,\n\tMonthlyReportRow,\n\tMonthlyUsageSummary,\n\tPricingSource,\n\tTokenUsageEvent,\n} f"
},
{
"path": "apps/codex/src/pricing.ts",
"chars": 6016,
"preview": "import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';\nimport type { ModelPricing, PricingSource } from '"
},
{
"path": "apps/codex/src/run.ts",
"chars": 844,
"preview": "import process from 'node:process';\nimport { cli } from 'gunshi';\nimport { description, name, version } from '../package"
},
{
"path": "apps/codex/src/session-report.ts",
"chars": 6233,
"preview": "import type {\n\tModelPricing,\n\tModelUsage,\n\tPricingSource,\n\tSessionReportRow,\n\tSessionUsageSummary,\n\tTokenUsageEvent,\n} f"
},
{
"path": "apps/codex/src/token-utils.ts",
"chars": 2010,
"preview": "import type { ModelPricing, TokenUsageDelta } from './_types.ts';\nimport { formatCurrency, formatTokens } from '@ccusage"
},
{
"path": "apps/codex/tsconfig.json",
"chars": 650,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"lib\": [\"ESNext\"],\n\t\t\"moduleDetection\": \"force\",\n\t\t\"module\": \"Preserve\","
},
{
"path": "apps/codex/tsdown.config.ts",
"chars": 474,
"preview": "import { defineConfig } from 'tsdown';\nimport Macros from 'unplugin-macros/rolldown';\n\nexport default defineConfig({\n\ten"
},
{
"path": "apps/codex/vitest.config.ts",
"chars": 297,
"preview": "import Macros from 'unplugin-macros/vite';\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n"
},
{
"path": "apps/mcp/CLAUDE.md",
"chars": 3393,
"preview": "# CLAUDE.md - MCP Package\n\nThis package provides the MCP (Model Context Protocol) server implementation for ccusage data"
},
{
"path": "apps/mcp/README.md",
"chars": 4020,
"preview": "<div align=\"center\">\n <img src=\"https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/logo.svg\" alt=\"ccusage"
},
{
"path": "apps/mcp/eslint.config.js",
"chars": 273,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\n/** @type {import('eslint').Linter.FlatConfig[]} */\nconst config "
},
{
"path": "apps/mcp/package.json",
"chars": 1987,
"preview": "{\n\t\"name\": \"@ccusage/mcp\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"description\": \"MCP server implementation for ccus"
},
{
"path": "apps/mcp/src/ccusage.ts",
"chars": 4179,
"preview": "import type { CliInvocation } from './cli-utils.ts';\nimport { z } from 'zod';\nimport { createCliInvocation, executeCliCo"
},
{
"path": "apps/mcp/src/cli-utils.ts",
"chars": 2707,
"preview": "import { createRequire } from 'node:module';\nimport path from 'node:path';\nimport process from 'node:process';\nimport sp"
},
{
"path": "apps/mcp/src/codex.ts",
"chars": 3552,
"preview": "import type { CliInvocation } from './cli-utils.ts';\nimport { z } from 'zod';\nimport { createCliInvocation, executeCliCo"
},
{
"path": "apps/mcp/src/command.ts",
"chars": 2651,
"preview": "import type { LoadOptions } from 'ccusage/data-loader';\nimport process from 'node:process';\nimport { serve } from '@hono"
},
{
"path": "apps/mcp/src/consts.ts",
"chars": 83,
"preview": "export const DEFAULT_LOCALE = 'en-CA';\nexport const DATE_FILTER_REGEX = /^\\d{8}$/;\n"
},
{
"path": "apps/mcp/src/index.ts",
"chars": 124,
"preview": "#!/usr/bin/env node\n\nimport { run } from './command.ts';\n\n// eslint-disable-next-line antfu/no-top-level-await\nawait run"
},
{
"path": "apps/mcp/src/mcp-utils.ts",
"chars": 404,
"preview": "import type { LoadOptions } from 'ccusage/data-loader';\nimport { getClaudePaths } from 'ccusage/data-loader';\n\nexport fu"
},
{
"path": "apps/mcp/src/mcp.ts",
"chars": 26349,
"preview": "/**\n * @fileoverview MCP (Model Context Protocol) server implementation\n *\n * This module provides MCP server functional"
},
{
"path": "apps/mcp/tsconfig.json",
"chars": 671,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"jsx\": \"react-jsx\",\n\t\t\"lib\": [\"ESNext\"],\n\t\t\"moduleDetection\": \"force\",\n\t"
},
{
"path": "apps/mcp/tsdown.config.ts",
"chars": 395,
"preview": "import { defineConfig } from 'tsdown';\n\nexport default defineConfig({\n\tentry: ['src/index.ts'],\n\toutDir: 'dist',\n\tformat"
},
{
"path": "apps/mcp/vitest.config.ts",
"chars": 166,
"preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n\ttest: {\n\t\twatch: false,\n\t\tincludeSource: ["
},
{
"path": "apps/opencode/CLAUDE.md",
"chars": 1743,
"preview": "# OpenCode CLI Notes\n\n## Log Sources\n\n- OpenCode session usage is recorded under `${OPENCODE_DATA_DIR:-~/.local/share/op"
},
{
"path": "apps/opencode/README.md",
"chars": 6195,
"preview": "<div align=\"center\">\n <img src=\"https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/logo.svg\" alt=\"ccusage"
},
{
"path": "apps/opencode/eslint.config.js",
"chars": 273,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\n/** @type {import('eslint').Linter.FlatConfig[]} */\nconst config "
},
{
"path": "apps/opencode/package.json",
"chars": 1811,
"preview": "{\n\t\"name\": \"@ccusage/opencode\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"description\": \"Usage analysis tool for OpenC"
},
{
"path": "apps/opencode/src/commands/daily.ts",
"chars": 4960,
"preview": "import { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tfor"
},
{
"path": "apps/opencode/src/commands/index.ts",
"chars": 170,
"preview": "export { dailyCommand } from './daily';\nexport { monthlyCommand } from './monthly';\nexport { sessionCommand } from './se"
},
{
"path": "apps/opencode/src/commands/monthly.ts",
"chars": 5005,
"preview": "import { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tfor"
},
{
"path": "apps/opencode/src/commands/session.ts",
"chars": 7690,
"preview": "import { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tfor"
},
{
"path": "apps/opencode/src/commands/weekly.ts",
"chars": 6819,
"preview": "import { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';\nimport {\n\taddEmptySeparatorRow,\n\tformatCurrency,\n\tfor"
},
{
"path": "apps/opencode/src/cost-utils.ts",
"chars": 1336,
"preview": "import type { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';\nimport type { LoadedUsageEntry } from './data-lo"
},
{
"path": "apps/opencode/src/data-loader.ts",
"chars": 8979,
"preview": "/**\n * @fileoverview Data loading utilities for OpenCode usage analysis\n *\n * This module provides functions for loading"
},
{
"path": "apps/opencode/src/index.ts",
"chars": 120,
"preview": "#!/usr/bin/env node\n\nimport { run } from './run.ts';\n\n// eslint-disable-next-line antfu/no-top-level-await\nawait run();\n"
},
{
"path": "apps/opencode/src/logger.ts",
"chars": 194,
"preview": "import { createLogger, log as internalLog } from '@ccusage/internal/logger';\n\nimport { name } from '../package.json';\n\ne"
},
{
"path": "apps/opencode/src/run.ts",
"chars": 810,
"preview": "import process from 'node:process';\nimport { cli } from 'gunshi';\nimport { description, name, version } from '../package"
},
{
"path": "apps/opencode/tsconfig.json",
"chars": 650,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"lib\": [\"ESNext\"],\n\t\t\"moduleDetection\": \"force\",\n\t\t\"module\": \"Preserve\","
},
{
"path": "apps/opencode/tsdown.config.ts",
"chars": 221,
"preview": "import { defineConfig } from 'tsdown';\n\nexport default defineConfig({\n\tentry: ['src/index.ts'],\n\tformat: ['esm'],\n\tclean"
},
{
"path": "apps/opencode/vitest.config.ts",
"chars": 274,
"preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n\ttest: {\n\t\tglobals: true,\n\t\tincludeSource: "
},
{
"path": "apps/pi/CLAUDE.md",
"chars": 3061,
"preview": "# CLAUDE.md - Pi Package\n\nThis package provides usage tracking for pi-agent.\n\n## Package Overview\n\n**Name**: `@ccusage/p"
},
{
"path": "apps/pi/README.md",
"chars": 5202,
"preview": "<div align=\"center\">\n <img src=\"https://cdn.jsdelivr.net/gh/ryoppippi/ccusage@main/docs/public/logo.svg\" alt=\"ccusage"
},
{
"path": "apps/pi/eslint.config.js",
"chars": 273,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\n/** @type {import('eslint').Linter.FlatConfig[]} */\nconst config "
},
{
"path": "apps/pi/package.json",
"chars": 2007,
"preview": "{\n\t\"name\": \"@ccusage/pi\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"description\": \"Pi-agent usage tracking - unified C"
},
{
"path": "apps/pi/src/_consts.ts",
"chars": 273,
"preview": "import { homedir } from 'node:os';\nimport path from 'node:path';\n\nexport const USER_HOME_DIR = homedir();\n\nexport const "
},
{
"path": "apps/pi/src/_pi-agent.ts",
"chars": 7899,
"preview": "import path from 'node:path';\nimport process from 'node:process';\nimport { isDirectorySync } from 'path-type';\nimport * "
},
{
"path": "apps/pi/src/_types.ts",
"chars": 339,
"preview": "import * as v from 'valibot';\n\nconst isoTimestampRegex = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?(?:Z|[+-]\\d{2}"
},
{
"path": "apps/pi/src/commands/daily.ts",
"chars": 3092,
"preview": "import process from 'node:process';\nimport {\n\taddEmptySeparatorRow,\n\tcreateUsageReportTable,\n\tformatDateCompact,\n\tformat"
},
{
"path": "apps/pi/src/commands/index.ts",
"chars": 137,
"preview": "export { dailyCommand } from './daily.ts';\nexport { monthlyCommand } from './monthly.ts';\nexport { sessionCommand } from"
},
{
"path": "apps/pi/src/commands/monthly.ts",
"chars": 3107,
"preview": "import process from 'node:process';\nimport {\n\taddEmptySeparatorRow,\n\tcreateUsageReportTable,\n\tformatDateCompact,\n\tformat"
},
{
"path": "apps/pi/src/commands/session.ts",
"chars": 3309,
"preview": "import path from 'node:path';\nimport process from 'node:process';\nimport {\n\taddEmptySeparatorRow,\n\tcreateUsageReportTabl"
},
{
"path": "apps/pi/src/data-loader.ts",
"chars": 10571,
"preview": "import fs from 'node:fs';\nimport readline from 'node:readline';\nimport { glob } from 'tinyglobby';\nimport * as v from 'v"
},
{
"path": "apps/pi/src/index.ts",
"chars": 781,
"preview": "#!/usr/bin/env node\n\nimport process from 'node:process';\nimport { cli } from 'gunshi';\nimport { description, name, versi"
},
{
"path": "apps/pi/src/logger.ts",
"chars": 194,
"preview": "import { createLogger, log as internalLog } from '@ccusage/internal/logger';\n\nimport { name } from '../package.json';\n\ne"
},
{
"path": "apps/pi/tsconfig.json",
"chars": 685,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"jsx\": \"react-jsx\",\n\t\t\"lib\": [\"ESNext\"],\n\t\t\"moduleDetection\": \"force\",\n\t"
},
{
"path": "apps/pi/tsdown.config.ts",
"chars": 395,
"preview": "import { defineConfig } from 'tsdown';\n\nexport default defineConfig({\n\tentry: ['src/index.ts'],\n\toutDir: 'dist',\n\tformat"
},
{
"path": "apps/pi/vitest.config.ts",
"chars": 166,
"preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n\ttest: {\n\t\twatch: false,\n\t\tincludeSource: ["
},
{
"path": "ccusage.example.json",
"chars": 566,
"preview": "{\n\t\"$schema\": \"./apps/ccusage/config-schema.json\",\n\t\"defaults\": {\n\t\t\"json\": true,\n\t\t\"mode\": \"auto\",\n\t\t\"timezone\": \"Asia/"
},
{
"path": "docs/.gitignore",
"chars": 233,
"preview": "# VitePress build output\n.vitepress/dist/\n.vitepress/cache/\n\n# Generated documentation\napi/\n\n# Dependencies\nnode_modules"
},
{
"path": "docs/.vitepress/config.ts",
"chars": 5665,
"preview": "import type { DefaultTheme } from 'vitepress';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport "
},
{
"path": "docs/CLAUDE.md",
"chars": 4440,
"preview": "# CLAUDE.md - Documentation\n\nThis directory contains the VitePress-based documentation website for ccusage.\n\n## Package "
},
{
"path": "docs/eslint.config.js",
"chars": 136,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\nexport default ryoppippi({\n\ttype: 'app',\n\tmarkdown: true,\n\tstylis"
},
{
"path": "docs/guide/blocks-reports.md",
"chars": 10801,
"preview": "# Blocks Reports\n\nBlocks reports show your Claude Code usage grouped by 5-hour billing windows, helping you understand C"
},
{
"path": "docs/guide/cli-options.md",
"chars": 7008,
"preview": "# Command-Line Options\n\nccusage provides extensive command-line options to customize its behavior. These options take pr"
},
{
"path": "docs/guide/codex/daily.md",
"chars": 1282,
"preview": "# Codex Daily Report (Beta)\n\nThe `daily` command mirrors ccusage's daily report but operates on Codex CLI session logs.\n"
},
{
"path": "docs/guide/codex/index.md",
"chars": 5200,
"preview": "# Codex CLI Overview (Beta)\n\n\n\n> ⚠️ The Codex companion CLI is experimental. E"
},
{
"path": "docs/guide/codex/monthly.md",
"chars": 1346,
"preview": "# Codex Monthly Report (Beta)\n\n\n\nThe `monthly` command mirrors ccusage's mon"
},
{
"path": "docs/guide/codex/session.md",
"chars": 2028,
"preview": "# Codex Session Report (Beta)\n\nThe `session` command groups Codex CLI usage by individual sessions so you can spot long-"
},
{
"path": "docs/guide/config-files.md",
"chars": 8376,
"preview": "# Configuration Files\n\nccusage supports JSON configuration files for persistent settings. Configuration files allow you "
},
{
"path": "docs/guide/configuration.md",
"chars": 7501,
"preview": "# Configuration Overview\n\nccusage provides multiple ways to configure its behavior, allowing you to customize it for you"
},
{
"path": "docs/guide/cost-modes.md",
"chars": 9576,
"preview": "# Cost Modes\n\nccusage supports three different cost calculation modes to handle various scenarios and data sources. Unde"
},
{
"path": "docs/guide/custom-paths.md",
"chars": 9482,
"preview": "# Custom Paths\n\nccusage supports flexible path configuration to handle various Claude Code installation scenarios and cu"
},
{
"path": "docs/guide/daily-reports.md",
"chars": 7620,
"preview": "# Daily Reports\n\n\n\nDail"
},
{
"path": "docs/guide/directory-detection.md",
"chars": 2891,
"preview": "# Directory Detection\n\nccusage automatically detects and manages Claude Code data directories.\n\n## Default Directory Loc"
},
{
"path": "docs/guide/environment-variables.md",
"chars": 5623,
"preview": "# Environment Variables\n\nccusage supports several environment variables for configuration and customization. Environment"
},
{
"path": "docs/guide/getting-started.md",
"chars": 4294,
"preview": "# Getting Started\n\nWelcome to ccusage! This guide will help you get up and running with analyzing your Claude Code usage"
},
{
"path": "docs/guide/index.md",
"chars": 4219,
"preview": "# Introduction\n\n\n\n**ccusage** (claude-code"
},
{
"path": "docs/guide/installation.md",
"chars": 5296,
"preview": "# Installation\n\nccusage can be installed and used in several ways depending on your preferences and use case.\n\n## Why No"
},
{
"path": "docs/guide/json-output.md",
"chars": 11321,
"preview": "# JSON Output\n\nccusage supports structured JSON output for all report types, making it easy to integrate with other tool"
},
{
"path": "docs/guide/library-usage.md",
"chars": 3634,
"preview": "# Library Usage\n\nWhile **ccusage** is primarily known as a CLI tool, it can also be used as a library in your JavaScript"
},
{
"path": "docs/guide/live-monitoring.md",
"chars": 1684,
"preview": "# Live Monitoring (Removed)\n\n![Live monitoring dashboard showing real-time token usage, burn rate, and cost projections]"
},
{
"path": "docs/guide/mcp-server.md",
"chars": 3838,
"preview": "# MCP Server\n\nThe ccusage MCP server now lives in the dedicated `@ccusage/mcp` package. This keeps the main CLI lightwei"
},
{
"path": "docs/guide/monthly-reports.md",
"chars": 6202,
"preview": "# Monthly Reports\n\nMonthly reports aggregate your Claude Code usage by calendar month, providing a high-level view of yo"
},
{
"path": "docs/guide/opencode/index.md",
"chars": 3464,
"preview": "# OpenCode CLI Overview (Beta)\n\n> The OpenCode companion CLI is experimental. Expect breaking changes while both ccusage"
},
{
"path": "docs/guide/pi/index.md",
"chars": 10179,
"preview": "# Pi-Agent Integration\n\nThe `@ccusage/pi` package provides usage tracking for [pi-agent](https://github.com/badlogic/pi-"
},
{
"path": "docs/guide/related-projects.md",
"chars": 1177,
"preview": "# Related Projects\n\nProjects that use ccusage internally or extend its functionality:\n\n## Desktop Applications\n\n- [claud"
},
{
"path": "docs/guide/session-reports.md",
"chars": 9748,
"preview": "# Session Reports\n\nSession reports show your Claude Code usage grouped by individual conversation sessions, making it ea"
},
{
"path": "docs/guide/sponsors.md",
"chars": 1308,
"preview": "# Sponsors\n\nSupport ccusage development by becoming a sponsor! Your contribution helps maintain and improve this tool.\n\n"
},
{
"path": "docs/guide/statusline.md",
"chars": 8703,
"preview": "# Statusline Integration (Beta) 🚀\n\nDisplay real-time usage statistics in your Claude Code status line.\n\n## Overview\n\nThe"
},
{
"path": "docs/guide/weekly-reports.md",
"chars": 5842,
"preview": "# Weekly Reports\n\nWeekly reports aggregate your Claude Code usage by week, providing a mid-range view between daily and "
},
{
"path": "docs/index.md",
"chars": 2885,
"preview": "---\nlayout: home\n\nhero:\n name: ccusage\n text: Claude Code Usage Analysis\n tagline: A powerful CLI tool for analyzing "
},
{
"path": "docs/package.json",
"chars": 1286,
"preview": "{\n\t\"name\": \"@ccusage/docs\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"private\": true,\n\t\"description\": \"Documentation f"
},
{
"path": "docs/tsconfig.json",
"chars": 353,
"preview": "{\n\t\"extends\": \"./node_modules/ccusage/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"resolveJsonModule\": true,\n\t\t\"types\": [\"bu"
},
{
"path": "docs/typedoc.config.ts",
"chars": 1545,
"preview": "import type { TypeDocOptions } from 'typedoc';\nimport type { PluginOptions } from 'typedoc-plugin-markdown';\nimport { gl"
},
{
"path": "docs/update-api-index.ts",
"chars": 2614,
"preview": "#!/usr/bin/env bun\n/* eslint-disable antfu/no-top-level-await */\n/* eslint-disable no-console */\n\n/**\n * Post-processing"
},
{
"path": "docs/wrangler.jsonc",
"chars": 261,
"preview": "{\n\t\"$schema\": \"../node_modules/wrangler/config-schema.json\",\n\t\"name\": \"ccusage-guide\",\n\t\"compatibility_date\": \"2025-07-2"
},
{
"path": "eslint.config.js",
"chars": 190,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\nexport default ryoppippi({\n\ttype: 'lib',\n\tstylistic: false,\n\tigno"
},
{
"path": "flake.nix",
"chars": 1047,
"preview": "{\n description = \"Usage analysis tool for Claude Code\";\n\n inputs.nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\""
},
{
"path": "package.json",
"chars": 1733,
"preview": "{\n\t\"name\": \"ccusage-monorepo\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"private\": true,\n\t\"workspaces\": [\n\t\t\"apps/*\",\n"
},
{
"path": "packages/internal/CLAUDE.md",
"chars": 3889,
"preview": "# CLAUDE.md - Internal Package\n\nThis package contains shared internal utilities for the ccusage monorepo.\n\n## Package Ov"
},
{
"path": "packages/internal/eslint.config.js",
"chars": 273,
"preview": "import { ryoppippi } from '@ryoppippi/eslint-config';\n\n/** @type {import('eslint').Linter.FlatConfig[]} */\nconst config "
},
{
"path": "packages/internal/package.json",
"chars": 808,
"preview": "{\n\t\"name\": \"@ccusage/internal\",\n\t\"type\": \"module\",\n\t\"version\": \"18.0.10\",\n\t\"private\": true,\n\t\"description\": \"Shared inte"
},
{
"path": "packages/internal/src/constants.ts",
"chars": 243,
"preview": "/**\n * Default locale for date formatting (en-CA provides YYYY-MM-DD ISO format)\n * @constant\n */\nexport const DEFAULT_L"
},
{
"path": "packages/internal/src/format.ts",
"chars": 681,
"preview": "/**\n * Format a number as tokens with locale-specific formatting\n * @param value - Token count to format\n * @returns For"
},
{
"path": "packages/internal/src/logger.ts",
"chars": 538,
"preview": "import type { ConsolaInstance } from 'consola';\nimport process from 'node:process';\nimport { consola } from 'consola';\n\n"
},
{
"path": "packages/internal/src/pricing-fetch-utils.ts",
"chars": 1386,
"preview": "import type { LiteLLMModelPricing } from './pricing.ts';\nimport * as v from 'valibot';\nimport { LITELLM_PRICING_URL, lit"
},
{
"path": "packages/internal/src/pricing.ts",
"chars": 19588,
"preview": "import { Result } from '@praha/byethrow';\nimport * as v from 'valibot';\n\nexport const LITELLM_PRICING_URL =\n\t'https://ra"
},
{
"path": "packages/internal/tsconfig.json",
"chars": 601,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ESNext\",\n\t\t\"lib\": [\"ESNext\"],\n\t\t\"moduleDetection\": \"force\",\n\t\t\"module\": \"Preserve\","
}
]
// ... and 11 more files (download for full content)
About this extraction
This page contains the full source code of the ryoppippi/ccusage GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 211 files (934.0 KB), approximately 263.6k tokens, and a symbol index with 472 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.