Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Building and Installing ccusage Locally

This guide explains how to build and install a custom version of ccusage for local development and testing.

## Prerequisites

- [Bun](https://bun.sh/) runtime (latest version)
- Node.js (for global installation via npm)
- Git (for version control)

## Quick Start

```bash
# 1. Clone and navigate to the project
git clone <your-fork-url>
cd ccusage

# 2. Install dependencies
bun install

# 3. Build the project
bun run build

# 4. Install globally with custom name
npm install -g .
```

## Build Scripts

The project includes these build-related scripts:

```bash
# Core build commands
bun run build # Build distribution files
bun run typecheck # Type check TypeScript
bun run format # Format and lint code
bun run test # Run test suite

# Quality assurance
bun run release # Full release workflow (lint + typecheck + test + build)

# Development
bun run start # Run from source
```

## Troubleshooting

### Build Fails

```bash
# Clean and rebuild
rm -rf dist/ node_modules/
bun install
bun run build
```

### Global Install Fails

```bash
# Try with npm instead of bun
npm install -g .

# Or try with sudo (macOS/Linux)
sudo npm install -g .
```

### Command Not Found

```bash
# Check if it's in your PATH
which your-custom-name

# Check npm global packages
npm list -g --depth=0

# Verify npm global bin directory
npm config get prefix
```

### Permission Issues

```bash
# Fix npm permissions (macOS/Linux)
mkdir ~/.npm-global
npm config set prefix '~/.npm-global'

# Add to ~/.bashrc or ~/.zshrc
export PATH=~/.npm-global/bin:$PATH
```

## New Features

- **Cost-based live monitoring**: `--cost-limit` option with numeric values or `max`
- **Model filtering**: `--model opus` or `--model opus,sonnet` for specific models
- **Per-model cost limits**: Historical maximums calculated per model
- **Improved projections**: Activity-aware calculations for sparse model usage
8 changes: 8 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"workspaces": {
"": {
"name": "ccusage",
"dependencies": {
"hg-ccusage": ".",
"g": "^2.0.1",
},
"devDependencies": {
"@antfu/utils": "^9.2.0",
"@core/errorutil": "npm:@jsr/core__errorutil",
Expand Down Expand Up @@ -1071,6 +1075,8 @@

"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],

"g": ["g@2.0.1", "", {}, "sha512-Fi6Ng5fZ/ANLQ15H11hCe+09sgUoNvDEBevVgx3KoYOhsH5iLNPn54hx0jPZ+3oSWr+xajnp2Qau9VmPsc7hTA=="],

"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],

"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
Expand Down Expand Up @@ -1117,6 +1123,8 @@

"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],

"hg-ccusage": ["hg-ccusage@file:", { "devDependencies": { "@antfu/utils": "^9.2.0", "@core/errorutil": "npm:@jsr/core__errorutil", "@hono/mcp": "^0.1.0", "@hono/node-server": "^1.14.4", "@jsr/std__async": "1", "@modelcontextprotocol/sdk": "^1.13.0", "@oxc-project/runtime": "^0.73.2", "@ryoppippi/eslint-config": "^0.3.7", "@types/bun": "^1.2.16", "@typescript/native-preview": "^7.0.0-dev.20250619.1", "ansi-escapes": "^7.0.0", "bumpp": "^10.2.0", "clean-pkg-json": "^1.3.0", "cli-table3": "^0.6.5", "consola": "^3.4.2", "es-toolkit": "^1.39.3", "eslint": "^9.29.0", "eslint-plugin-format": "^1.0.1", "fast-sort": "^3.4.1", "fs-fixture": "^2.8.1", "gunshi": "^0.26.3", "hono": "^4.8.0", "lint-staged": "^16.1.2", "path-type": "^6.0.0", "picocolors": "^1.1.1", "pretty-ms": "^9.2.0", "publint": "^0.3.12", "rollup-plugin-node-externals": "^8.0.1", "simple-git-hooks": "^2.13.0", "sort-package-json": "^3.2.1", "string-width": "^7.2.0", "tinyglobby": "^0.2.14", "tsdown": "^0.12.8", "type-fest": "^4.41.0", "unplugin-macros": "^0.17.0", "unplugin-unused": "^0.5.1", "vitest": "^3.2.4", "xdg-basedir": "^5.1.0", "zod": "^3.25.67" }, "bin": "./dist/index.js" }],

"hono": ["hono@4.8.2", "", {}, "sha512-hM+1RIn9PK1I6SiTNS6/y7O1mvg88awYLFEuEtoiMtRyT3SD2iu9pSFgbBXT3b1Ua4IwzvSTLvwO0SEhDxCi4w=="],

"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "ccusage",
"name": "hg-ccusage",
"type": "module",
"version": "15.2.0",
"description": "Usage analysis tool for Claude Code",
Expand Down Expand Up @@ -51,6 +51,10 @@
"test": "vitest",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"g": "^2.0.1",
"hg-ccusage": "."
},
"devDependencies": {
"@antfu/utils": "^9.2.0",
"@core/errorutil": "npm:@jsr/core__errorutil",
Expand Down
83 changes: 80 additions & 3 deletions src/_live-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type LiveMonitorConfig = {
sessionDurationHours: number;
mode: CostMode;
order: SortOrder;
models?: string[];
};

/**
Expand Down Expand Up @@ -145,7 +146,7 @@ export class LiveMonitor implements Disposable {
}
}

// Generate blocks and find active one
// Generate blocks from ALL entries first (preserve session structure)
const blocks = identifySessionBlocks(
this.allEntries,
this.config.sessionDurationHours,
Expand All @@ -156,8 +157,84 @@ export class LiveMonitor implements Disposable {
? blocks
: blocks.reverse();

// Find active block
return sortedBlocks.find(block => block.isActive) ?? null;
// Find active block based on session structure (not filtered data)
const activeBlock = sortedBlocks.find(block => block.isActive);

if (activeBlock == null) {
return null;
}

// Apply model filtering to the active block's entries only
if (this.config.models != null && this.config.models.length > 0) {
return this.filterBlockByModels(activeBlock, this.config.models);
}

return activeBlock;
}

/**
* Filters a session block's entries by specified models while preserving block structure
* @param block - Original session block
* @param models - Array of model names to filter by
* @returns New session block with filtered entries and recalculated aggregates
*/
private filterBlockByModels(block: SessionBlock, models: string[]): SessionBlock {
// Filter entries by models with partial matching
const filteredEntries = block.entries.filter((entry) => {
const entryModel = entry.model.toLowerCase();
return models.some((filterModel) => {
const lowerFilterModel = filterModel.toLowerCase();
return entryModel.includes(lowerFilterModel) || lowerFilterModel.includes(entryModel);
});
});

// If no entries match, return empty block with preserved structure
if (filteredEntries.length === 0) {
return {
...block,
entries: [],
tokenCounts: {
inputTokens: 0,
outputTokens: 0,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
},
costUSD: 0,
models: [],
};
}

// Recalculate aggregated data for filtered entries
const tokenCounts = {
inputTokens: 0,
outputTokens: 0,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
};

let totalCost = 0;
const uniqueModels = new Set<string>();

for (const entry of filteredEntries) {
tokenCounts.inputTokens += entry.usage.inputTokens;
tokenCounts.outputTokens += entry.usage.outputTokens;
tokenCounts.cacheCreationInputTokens += entry.usage.cacheCreationInputTokens;
tokenCounts.cacheReadInputTokens += entry.usage.cacheReadInputTokens;

if (entry.costUSD != null) {
totalCost += entry.costUSD;
}

uniqueModels.add(entry.model);
}

return {
...block,
entries: filteredEntries,
tokenCounts,
costUSD: totalCost,
models: Array.from(uniqueModels).sort(),
};
}

/**
Expand Down
Loading