Skip to content

Commit acfacba

Browse files
authored
feat(amp): add Amp CLI usage analysis package (#785)
1 parent 9827559 commit acfacba

19 files changed

+1616
-0
lines changed

apps/amp/CLAUDE.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Amp CLI Notes
2+
3+
## Log Sources
4+
5+
- 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`).
6+
- Each thread is stored as a JSON file (not JSONL) named `T-{uuid}.json`.
7+
- Token usage is extracted from the `usageLedger.events[]` array in each thread file.
8+
- Cache token information (creation/read) is extracted from `messages[].usage` for detailed breakdown.
9+
10+
## Token Fields
11+
12+
- `inputTokens`: total input tokens sent to the model.
13+
- `outputTokens`: output tokens (completion text).
14+
- `cacheCreationInputTokens`: tokens used for cache creation (from message usage).
15+
- `cacheReadInputTokens`: tokens read from cache (from message usage).
16+
- `totalTokens`: sum of input and output tokens.
17+
18+
## Credits
19+
20+
- Amp uses a credits-based billing system in addition to standard token counts.
21+
- Each usage event includes a `credits` field representing the billing cost in Amp's credit system.
22+
- Credits are displayed alongside USD cost estimates in reports.
23+
24+
## Cost Calculation
25+
26+
- Pricing is pulled from LiteLLM's public JSON (`model_prices_and_context_window.json`).
27+
- Amp primarily uses Anthropic Claude models (Haiku, Sonnet, Opus variants).
28+
- Cost formula per model:
29+
- Input: `inputTokens / 1_000_000 * input_cost_per_mtoken`
30+
- Cached input read: `cacheReadInputTokens / 1_000_000 * cached_input_cost_per_mtoken`
31+
- Cache creation: `cacheCreationInputTokens / 1_000_000 * cache_creation_cost_per_mtoken`
32+
- Output: `outputTokens / 1_000_000 * output_cost_per_mtoken`
33+
34+
## CLI Usage
35+
36+
- Treat Amp as a sibling to `apps/ccusage`, `apps/codex`, and `apps/opencode`.
37+
- Reuse shared packages (`@ccusage/terminal`, `@ccusage/internal`) wherever possible.
38+
- Amp is packaged as a bundled CLI. Keep every runtime dependency in `devDependencies`.
39+
- Entry point uses Gunshi framework with subcommands: `daily`, `monthly`, `session`.
40+
- Data discovery relies on `AMP_DATA_DIR` environment variable.
41+
- Default path: `~/.local/share/amp`.
42+
43+
## Available Commands
44+
45+
- `ccusage-amp daily` - Show daily usage report
46+
- `ccusage-amp monthly` - Show monthly usage report
47+
- `ccusage-amp session` - Show usage by thread (session)
48+
- Add `--json` flag for JSON output format
49+
- Add `--compact` flag for compact table mode
50+
51+
## Testing Notes
52+
53+
- Tests rely on `fs-fixture` with `using` to ensure cleanup.
54+
- All vitest blocks live alongside implementation files via `if (import.meta.vitest != null)`.
55+
- Vitest globals are enabled - use `describe`, `it`, `expect` directly without imports.
56+
- **CRITICAL**: NEVER use `await import()` dynamic imports anywhere, especially in test blocks.
57+
58+
## Data Structure
59+
60+
Amp thread files have the following structure:
61+
62+
```json
63+
{
64+
"id": "T-{uuid}",
65+
"created": 1700000000000,
66+
"title": "Thread Title",
67+
"messages": [
68+
{
69+
"role": "assistant",
70+
"messageId": 1,
71+
"usage": {
72+
"model": "claude-haiku-4-5-20251001",
73+
"inputTokens": 100,
74+
"outputTokens": 50,
75+
"cacheCreationInputTokens": 500,
76+
"cacheReadInputTokens": 200,
77+
"credits": 1.5
78+
}
79+
}
80+
],
81+
"usageLedger": {
82+
"events": [
83+
{
84+
"id": "event-uuid",
85+
"timestamp": "2025-11-23T10:00:00.000Z",
86+
"model": "claude-haiku-4-5-20251001",
87+
"credits": 1.5,
88+
"tokens": {
89+
"input": 100,
90+
"output": 50
91+
},
92+
"operationType": "inference",
93+
"fromMessageId": 0,
94+
"toMessageId": 1
95+
}
96+
]
97+
}
98+
}
99+
```
100+
101+
## Environment Variables
102+
103+
- `AMP_DATA_DIR` - Custom Amp data directory path (defaults to `~/.local/share/amp`)
104+
- `LOG_LEVEL` - Control logging verbosity (0=silent, 1=warn, 2=log, 3=info, 4=debug, 5=trace)

apps/amp/eslint.config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ryoppippi } from '@ryoppippi/eslint-config';
2+
3+
/** @type {import('eslint').Linter.FlatConfig[]} */
4+
const config = ryoppippi({
5+
type: 'app',
6+
}, {
7+
rules: {
8+
'test/no-importing-vitest-globals': 'error',
9+
},
10+
});
11+
12+
export default config;

apps/amp/package.json

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"name": "@ccusage/amp",
3+
"type": "module",
4+
"version": "0.1.0",
5+
"description": "Usage analysis tool for Amp CLI sessions",
6+
"author": "ryoppippi",
7+
"license": "MIT",
8+
"funding": "https://github.com/ryoppippi/ccusage?sponsor=1",
9+
"homepage": "https://github.com/ryoppippi/ccusage#readme",
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/ryoppippi/ccusage.git"
13+
},
14+
"bugs": {
15+
"url": "https://github.com/ryoppippi/ccusage/issues"
16+
},
17+
"main": "./dist/index.js",
18+
"module": "./dist/index.js",
19+
"bin": {
20+
"ccusage-amp": "./src/index.ts"
21+
},
22+
"files": [
23+
"dist"
24+
],
25+
"engines": {
26+
"node": ">=20.19.4"
27+
},
28+
"scripts": {
29+
"build": "tsdown",
30+
"format": "pnpm run lint --fix",
31+
"lint": "eslint --cache .",
32+
"prepack": "pnpm run build && clean-pkg-json",
33+
"prerelease": "pnpm run lint && pnpm run typecheck && pnpm run build",
34+
"start": "bun ./src/index.ts",
35+
"test": "TZ=UTC vitest",
36+
"typecheck": "tsgo --noEmit"
37+
},
38+
"devDependencies": {
39+
"@ccusage/internal": "workspace:*",
40+
"@ccusage/terminal": "workspace:*",
41+
"@praha/byethrow": "catalog:runtime",
42+
"@ryoppippi/eslint-config": "catalog:lint",
43+
"@typescript/native-preview": "catalog:types",
44+
"clean-pkg-json": "catalog:release",
45+
"eslint": "catalog:lint",
46+
"fast-sort": "catalog:runtime",
47+
"fs-fixture": "catalog:testing",
48+
"gunshi": "catalog:runtime",
49+
"path-type": "catalog:runtime",
50+
"picocolors": "catalog:runtime",
51+
"sort-package-json": "catalog:release",
52+
"tinyglobby": "catalog:runtime",
53+
"tsdown": "catalog:build",
54+
"unplugin-macros": "catalog:build",
55+
"unplugin-unused": "catalog:build",
56+
"valibot": "catalog:runtime",
57+
"vitest": "catalog:testing"
58+
},
59+
"publishConfig": {
60+
"bin": {
61+
"ccusage-amp": "./dist/index.js"
62+
}
63+
},
64+
"devEngines": {
65+
"runtime": [
66+
{
67+
"name": "node",
68+
"version": "^24.11.0",
69+
"onFail": "download"
70+
},
71+
{
72+
"name": "bun",
73+
"version": "^1.3.2",
74+
"onFail": "download"
75+
}
76+
]
77+
}
78+
}

apps/amp/src/_consts.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import path from 'node:path';
2+
import process from 'node:process';
3+
4+
/**
5+
* Environment variable name for custom Amp data directory
6+
*/
7+
export const AMP_DATA_DIR_ENV = 'AMP_DATA_DIR';
8+
9+
/**
10+
* Default Amp data directory path (~/.local/share/amp)
11+
*/
12+
const DEFAULT_AMP_PATH = '.local/share/amp';
13+
14+
/**
15+
* User home directory
16+
*/
17+
const USER_HOME_DIR = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd();
18+
19+
/**
20+
* Default Amp data directory (absolute path)
21+
*/
22+
export const DEFAULT_AMP_DIR = path.join(USER_HOME_DIR, DEFAULT_AMP_PATH);
23+
24+
/**
25+
* Amp threads subdirectory name
26+
*/
27+
export const AMP_THREADS_DIR_NAME = 'threads';
28+
29+
/**
30+
* Glob pattern for Amp thread files
31+
*/
32+
export const AMP_THREAD_GLOB = '**/*.json';
33+
34+
/**
35+
* Million constant for pricing calculations
36+
*/
37+
export const MILLION = 1_000_000;

apps/amp/src/_macro.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { LiteLLMModelPricing } from '@ccusage/internal/pricing';
2+
import {
3+
createPricingDataset,
4+
fetchLiteLLMPricingDataset,
5+
filterPricingDataset,
6+
} from '@ccusage/internal/pricing-fetch-utils';
7+
8+
const AMP_MODEL_PREFIXES = [
9+
'claude-',
10+
'anthropic/',
11+
];
12+
13+
function isAmpModel(modelName: string, _pricing: LiteLLMModelPricing): boolean {
14+
return AMP_MODEL_PREFIXES.some(prefix => modelName.startsWith(prefix));
15+
}
16+
17+
export async function prefetchAmpPricing(): Promise<Record<string, LiteLLMModelPricing>> {
18+
try {
19+
const dataset = await fetchLiteLLMPricingDataset();
20+
return filterPricingDataset(dataset, isAmpModel);
21+
}
22+
catch (error) {
23+
console.warn('Failed to prefetch Amp pricing data, proceeding with empty cache.', error);
24+
return createPricingDataset();
25+
}
26+
}

apps/amp/src/_types.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Token usage delta for a single event
3+
*/
4+
export type TokenUsageDelta = {
5+
inputTokens: number;
6+
cacheCreationInputTokens: number;
7+
cacheReadInputTokens: number;
8+
outputTokens: number;
9+
totalTokens: number;
10+
};
11+
12+
/**
13+
* Token usage event loaded from Amp thread files
14+
*/
15+
export type TokenUsageEvent = TokenUsageDelta & {
16+
timestamp: string;
17+
threadId: string;
18+
model: string;
19+
credits: number;
20+
operationType: string;
21+
};
22+
23+
/**
24+
* Model usage summary with token counts
25+
*/
26+
export type ModelUsage = TokenUsageDelta & {
27+
credits: number;
28+
};
29+
30+
/**
31+
* Daily usage summary
32+
*/
33+
export type DailyUsageSummary = {
34+
date: string;
35+
firstTimestamp: string;
36+
costUSD: number;
37+
credits: number;
38+
models: Map<string, ModelUsage>;
39+
} & TokenUsageDelta;
40+
41+
/**
42+
* Monthly usage summary
43+
*/
44+
export type MonthlyUsageSummary = {
45+
month: string;
46+
firstTimestamp: string;
47+
costUSD: number;
48+
credits: number;
49+
models: Map<string, ModelUsage>;
50+
} & TokenUsageDelta;
51+
52+
/**
53+
* Session (thread) usage summary
54+
*/
55+
export type SessionUsageSummary = {
56+
threadId: string;
57+
title: string;
58+
firstTimestamp: string;
59+
lastTimestamp: string;
60+
costUSD: number;
61+
credits: number;
62+
models: Map<string, ModelUsage>;
63+
} & TokenUsageDelta;
64+
65+
/**
66+
* Model pricing information
67+
*/
68+
export type ModelPricing = {
69+
inputCostPerMToken: number;
70+
cachedInputCostPerMToken: number;
71+
cacheCreationCostPerMToken: number;
72+
outputCostPerMToken: number;
73+
};
74+
75+
/**
76+
* Pricing source interface
77+
*/
78+
export type PricingSource = {
79+
getPricing: (model: string) => Promise<ModelPricing>;
80+
};
81+
82+
/**
83+
* Daily report row for JSON output
84+
*/
85+
export type DailyReportRow = {
86+
date: string;
87+
inputTokens: number;
88+
cacheCreationInputTokens: number;
89+
cacheReadInputTokens: number;
90+
outputTokens: number;
91+
totalTokens: number;
92+
costUSD: number;
93+
credits: number;
94+
models: Record<string, ModelUsage>;
95+
};
96+
97+
/**
98+
* Monthly report row for JSON output
99+
*/
100+
export type MonthlyReportRow = {
101+
month: string;
102+
inputTokens: number;
103+
cacheCreationInputTokens: number;
104+
cacheReadInputTokens: number;
105+
outputTokens: number;
106+
totalTokens: number;
107+
costUSD: number;
108+
credits: number;
109+
models: Record<string, ModelUsage>;
110+
};
111+
112+
/**
113+
* Session report row for JSON output
114+
*/
115+
export type SessionReportRow = {
116+
threadId: string;
117+
title: string;
118+
lastActivity: string;
119+
inputTokens: number;
120+
cacheCreationInputTokens: number;
121+
cacheReadInputTokens: number;
122+
outputTokens: number;
123+
totalTokens: number;
124+
costUSD: number;
125+
credits: number;
126+
models: Record<string, ModelUsage>;
127+
};

0 commit comments

Comments
 (0)