coderplan-tier 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/dist/collector.d.ts +2 -0
- package/dist/collector.js +101 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +39 -0
- package/dist/recommender.d.ts +2 -0
- package/dist/recommender.js +132 -0
- package/dist/renderer.d.ts +3 -0
- package/dist/renderer.js +189 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# coderplan-tier
|
|
2
|
+
|
|
3
|
+
Analyze your Claude Code usage and get a recommendation for the optimal pricing tier.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx coderplan-tier@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Problem
|
|
12
|
+
|
|
13
|
+
Developers need guidance on which Claude Code pricing tier best fits their usage patterns. This tool analyzes actual usage data and recommends the most cost-effective plan.
|
|
14
|
+
|
|
15
|
+
## Tier Overview
|
|
16
|
+
|
|
17
|
+
| Tier | Monthly Cost | Rate Limits | Best For |
|
|
18
|
+
|------|--------------|-------------|----------|
|
|
19
|
+
| API Only | Pay-per-token | Unlimited | Occasional users (<1 session/week) |
|
|
20
|
+
| Pro | $20/month | ~45 msgs/5hr, 40-80 hrs/week | Regular daily users |
|
|
21
|
+
| Max 5x | $100/month | 5× Pro limits | Heavy users, multiple projects |
|
|
22
|
+
| Max 20x | $200/month | 20× Pro limits | Power users, constant usage |
|
|
23
|
+
|
|
24
|
+
## How It Works
|
|
25
|
+
|
|
26
|
+
1. Reads your Claude Code usage data via [ccusage](https://github.com/ryoppippi/ccusage)
|
|
27
|
+
2. Analyzes usage patterns (sessions, tokens, costs, model mix)
|
|
28
|
+
3. Calculates break-even points for each tier
|
|
29
|
+
4. Recommends the tier that minimizes your total cost
|
|
30
|
+
|
|
31
|
+
## Example Output
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
╭─────────────────────────────────────────────────────────╮
|
|
35
|
+
│ CoderPlan Tier Recommendation │
|
|
36
|
+
╰─────────────────────────────────────────────────────────╯
|
|
37
|
+
|
|
38
|
+
Your Usage (Last 30 Days)
|
|
39
|
+
Sessions: 47
|
|
40
|
+
Active Days: 22/30
|
|
41
|
+
Total Tokens: 2.4M
|
|
42
|
+
API Cost: $67.32
|
|
43
|
+
Primary Model: Claude Sonnet (82%), Opus (18%)
|
|
44
|
+
|
|
45
|
+
Recommendation: Max 5x ($100/month)
|
|
46
|
+
Confidence: HIGH (consistent daily usage pattern)
|
|
47
|
+
|
|
48
|
+
Reasoning:
|
|
49
|
+
- You use Claude Code 22 days/month (heavy usage)
|
|
50
|
+
- Current API costs ($67/mo) exceed Pro break-even (~$30)
|
|
51
|
+
- Max 5x limits would cover your typical usage
|
|
52
|
+
|
|
53
|
+
Projected Savings: ~$47/month vs API-only
|
|
54
|
+
|
|
55
|
+
╭─────────────────────────────────────────────────────────╮
|
|
56
|
+
│ Tier Comparison │
|
|
57
|
+
├─────────────────────────────────────────────────────────┤
|
|
58
|
+
│ API Only $67/month (current spend) │
|
|
59
|
+
│ Pro $35/month ($20 + ~$15 overflow) │
|
|
60
|
+
│ Max 5x $100/month (no overflow expected) <-- │
|
|
61
|
+
│ Max 20x $200/month (overkill) │
|
|
62
|
+
╰─────────────────────────────────────────────────────────╯
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## CLI Options
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
coderplan-tier # Analyze last 30 days (default)
|
|
69
|
+
coderplan-tier --period 90 # Analyze last 90 days
|
|
70
|
+
coderplan-tier --json # Output as JSON
|
|
71
|
+
coderplan-tier --verbose # Show detailed metrics
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Break-Even Analysis
|
|
75
|
+
|
|
76
|
+
The recommendation algorithm considers:
|
|
77
|
+
|
|
78
|
+
| Tier | Break-Even Point |
|
|
79
|
+
|------|------------------|
|
|
80
|
+
| Pro ($20) | ~$25-30/month API spend |
|
|
81
|
+
| Max 5x ($100) | ~$120-150/month API spend |
|
|
82
|
+
| Max 20x ($200) | ~$250+/month API spend |
|
|
83
|
+
|
|
84
|
+
## Confidence Levels
|
|
85
|
+
|
|
86
|
+
The tool shows a confidence indicator based on usage variance:
|
|
87
|
+
|
|
88
|
+
- **HIGH**: Consistent daily usage, stable patterns
|
|
89
|
+
- **MEDIUM**: Some variation but clear trend
|
|
90
|
+
- **LOW**: Highly variable (heavy some weeks, idle others)
|
|
91
|
+
|
|
92
|
+
## API Token Pricing Reference
|
|
93
|
+
|
|
94
|
+
| Model | Input (per 1M) | Output (per 1M) |
|
|
95
|
+
|-------|----------------|-----------------|
|
|
96
|
+
| Haiku | $1 | $5 |
|
|
97
|
+
| Sonnet 4.5 | $3 | $15 |
|
|
98
|
+
| Opus 4.5 | $5 | $25 |
|
|
99
|
+
| Opus 4.1 | $15 | $75 |
|
|
100
|
+
|
|
101
|
+
## Requirements
|
|
102
|
+
|
|
103
|
+
- Node.js 18+
|
|
104
|
+
- Claude Code usage history (stored locally by Claude Code)
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
export async function getUsageData(days) {
|
|
3
|
+
const since = formatDateYYYYMMDD(new Date(Date.now() - days * 24 * 60 * 60 * 1000));
|
|
4
|
+
// Get daily data (more granular for active days calculation)
|
|
5
|
+
const dailyData = runCcusage("daily", since);
|
|
6
|
+
// Aggregate totals from daily data
|
|
7
|
+
let totalCost = 0;
|
|
8
|
+
let totalInputTokens = 0;
|
|
9
|
+
let totalOutputTokens = 0;
|
|
10
|
+
let totalCacheCreationTokens = 0;
|
|
11
|
+
let totalCacheReadTokens = 0;
|
|
12
|
+
const modelMap = new Map();
|
|
13
|
+
for (const day of dailyData.daily) {
|
|
14
|
+
totalCost += day.totalCost;
|
|
15
|
+
totalInputTokens += day.inputTokens;
|
|
16
|
+
totalOutputTokens += day.outputTokens;
|
|
17
|
+
totalCacheCreationTokens += day.cacheCreationTokens;
|
|
18
|
+
totalCacheReadTokens += day.cacheReadTokens;
|
|
19
|
+
// Aggregate model usage
|
|
20
|
+
for (const model of day.modelBreakdowns) {
|
|
21
|
+
const existing = modelMap.get(model.modelName);
|
|
22
|
+
if (existing) {
|
|
23
|
+
existing.cost += model.cost;
|
|
24
|
+
existing.inputTokens += model.inputTokens;
|
|
25
|
+
existing.outputTokens += model.outputTokens;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
modelMap.set(model.modelName, {
|
|
29
|
+
model: model.modelName,
|
|
30
|
+
cost: model.cost,
|
|
31
|
+
inputTokens: model.inputTokens,
|
|
32
|
+
outputTokens: model.outputTokens,
|
|
33
|
+
percentage: 0,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const activeDays = dailyData.daily.filter((d) => d.totalCost > 0).length;
|
|
39
|
+
const sessionCount = dailyData.daily.length; // Approximation: 1 session per active day
|
|
40
|
+
// Calculate percentages
|
|
41
|
+
const modelBreakdown = Array.from(modelMap.values()).map((m) => ({
|
|
42
|
+
...m,
|
|
43
|
+
percentage: totalCost > 0 ? (m.cost / totalCost) * 100 : 0,
|
|
44
|
+
}));
|
|
45
|
+
// Sort by cost descending
|
|
46
|
+
modelBreakdown.sort((a, b) => b.cost - a.cost);
|
|
47
|
+
const dailyUsage = dailyData.daily.map((d) => ({
|
|
48
|
+
date: d.date,
|
|
49
|
+
cost: d.totalCost,
|
|
50
|
+
sessions: 1,
|
|
51
|
+
}));
|
|
52
|
+
return {
|
|
53
|
+
totalCost,
|
|
54
|
+
totalInputTokens,
|
|
55
|
+
totalOutputTokens,
|
|
56
|
+
totalCacheCreationTokens,
|
|
57
|
+
totalCacheReadTokens,
|
|
58
|
+
sessionCount,
|
|
59
|
+
activeDays,
|
|
60
|
+
periodDays: days,
|
|
61
|
+
modelBreakdown,
|
|
62
|
+
dailyUsage,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function runCcusage(command, since) {
|
|
66
|
+
try {
|
|
67
|
+
const output = execSync(`npx ccusage@latest ${command} --json --since ${since}`, {
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
70
|
+
timeout: 30000,
|
|
71
|
+
});
|
|
72
|
+
return JSON.parse(output);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof Error && "stdout" in error) {
|
|
76
|
+
const stdout = error.stdout;
|
|
77
|
+
// ccusage may return empty data with exit code 0
|
|
78
|
+
if (stdout) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(stdout);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Fall through to error handling
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Return empty data structure for no data case
|
|
88
|
+
if (error instanceof Error &&
|
|
89
|
+
(error.message.includes("No usage data") ||
|
|
90
|
+
error.message.includes("ENOENT"))) {
|
|
91
|
+
return { monthly: [], daily: [] };
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Failed to run ccusage: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function formatDateYYYYMMDD(date) {
|
|
97
|
+
const year = date.getFullYear();
|
|
98
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
99
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
100
|
+
return `${year}${month}${day}`;
|
|
101
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { getUsageData } from "./collector.js";
|
|
4
|
+
import { recommend } from "./recommender.js";
|
|
5
|
+
import { renderRecommendation, renderJson } from "./renderer.js";
|
|
6
|
+
program
|
|
7
|
+
.name("coderplan-tier")
|
|
8
|
+
.description("Analyze Claude Code usage and get tier recommendations")
|
|
9
|
+
.version("0.1.0")
|
|
10
|
+
.option("-p, --period <days>", "Analysis period in days", "30")
|
|
11
|
+
.option("-j, --json", "Output as JSON")
|
|
12
|
+
.option("-v, --verbose", "Show detailed metrics")
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const days = parseInt(options.period, 10);
|
|
15
|
+
if (isNaN(days) || days < 1 || days > 365) {
|
|
16
|
+
console.error("Error: Period must be between 1 and 365 days");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const usage = await getUsageData(days);
|
|
21
|
+
if (usage.totalCost === 0 && usage.sessionCount === 0) {
|
|
22
|
+
console.log("No Claude Code usage data found for the specified period.");
|
|
23
|
+
console.log("Make sure you have used Claude Code and ccusage can access your data.");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
const recommendation = recommend(usage);
|
|
27
|
+
if (options.json) {
|
|
28
|
+
console.log(renderJson(usage, recommendation));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(renderRecommendation(usage, recommendation, options.verbose));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
program.parse();
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Tier subscription costs
|
|
2
|
+
const TIER_COSTS = {
|
|
3
|
+
"API Only": 0,
|
|
4
|
+
Pro: 20,
|
|
5
|
+
"Max 5x": 100,
|
|
6
|
+
"Max 20x": 200,
|
|
7
|
+
};
|
|
8
|
+
// Approximate break-even points (API cost at which tier becomes worthwhile)
|
|
9
|
+
const BREAK_EVEN = {
|
|
10
|
+
"API Only": 0,
|
|
11
|
+
Pro: 25,
|
|
12
|
+
"Max 5x": 120,
|
|
13
|
+
"Max 20x": 250,
|
|
14
|
+
};
|
|
15
|
+
// Approximate coverage ratios (what % of usage is covered by subscription)
|
|
16
|
+
// These are estimates based on typical usage patterns
|
|
17
|
+
const COVERAGE_RATIO = {
|
|
18
|
+
"API Only": 0,
|
|
19
|
+
Pro: 0.7, // Pro covers ~70% of moderate usage
|
|
20
|
+
"Max 5x": 0.9, // Max 5x covers ~90% of heavy usage
|
|
21
|
+
"Max 20x": 0.98, // Max 20x covers nearly all usage
|
|
22
|
+
};
|
|
23
|
+
export function recommend(usage) {
|
|
24
|
+
const monthlyMultiplier = 30 / usage.periodDays;
|
|
25
|
+
const currentMonthlyCost = usage.totalCost * monthlyMultiplier;
|
|
26
|
+
const activeDaysPerMonth = usage.activeDays * monthlyMultiplier;
|
|
27
|
+
// Calculate projected costs for each tier
|
|
28
|
+
const projectedCosts = calculateTierCosts(currentMonthlyCost);
|
|
29
|
+
// Select tier with lowest total cost
|
|
30
|
+
const tier = selectLowestCostTier(projectedCosts);
|
|
31
|
+
// Calculate confidence
|
|
32
|
+
const confidence = calculateConfidence(usage);
|
|
33
|
+
// Generate reasons
|
|
34
|
+
const reasons = generateReasons(tier, currentMonthlyCost, activeDaysPerMonth, usage);
|
|
35
|
+
// Mark recommended tier
|
|
36
|
+
for (const cost of projectedCosts) {
|
|
37
|
+
cost.isRecommended = cost.tier === tier;
|
|
38
|
+
}
|
|
39
|
+
// Calculate savings vs API-only
|
|
40
|
+
const apiOnlyCost = projectedCosts.find((c) => c.tier === "API Only")?.monthlyCost ?? currentMonthlyCost;
|
|
41
|
+
const recommendedCost = projectedCosts.find((c) => c.tier === tier)?.monthlyCost ?? currentMonthlyCost;
|
|
42
|
+
const savings = apiOnlyCost - recommendedCost;
|
|
43
|
+
return {
|
|
44
|
+
tier,
|
|
45
|
+
confidence,
|
|
46
|
+
reasons,
|
|
47
|
+
projectedCosts,
|
|
48
|
+
currentMonthlyCost,
|
|
49
|
+
savings: Math.max(0, savings),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function selectLowestCostTier(projectedCosts) {
|
|
53
|
+
// Find tier with minimum total cost
|
|
54
|
+
let minCost = Infinity;
|
|
55
|
+
let bestTier = "API Only";
|
|
56
|
+
for (const cost of projectedCosts) {
|
|
57
|
+
if (cost.monthlyCost < minCost) {
|
|
58
|
+
minCost = cost.monthlyCost;
|
|
59
|
+
bestTier = cost.tier;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return bestTier;
|
|
63
|
+
}
|
|
64
|
+
function calculateTierCosts(currentMonthlyCost) {
|
|
65
|
+
return ["API Only", "Pro", "Max 5x", "Max 20x"].map((tier) => {
|
|
66
|
+
const subscriptionCost = TIER_COSTS[tier];
|
|
67
|
+
const coverage = COVERAGE_RATIO[tier];
|
|
68
|
+
// Overflow = portion of usage not covered by subscription
|
|
69
|
+
const overflowCost = tier === "API Only"
|
|
70
|
+
? currentMonthlyCost
|
|
71
|
+
: Math.max(0, currentMonthlyCost * (1 - coverage));
|
|
72
|
+
return {
|
|
73
|
+
tier,
|
|
74
|
+
subscriptionCost,
|
|
75
|
+
overflowCost: Math.round(overflowCost * 100) / 100,
|
|
76
|
+
monthlyCost: Math.round((subscriptionCost + overflowCost) * 100) / 100,
|
|
77
|
+
isRecommended: false,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function calculateConfidence(usage) {
|
|
82
|
+
if (usage.dailyUsage.length < 7) {
|
|
83
|
+
return "LOW"; // Not enough data
|
|
84
|
+
}
|
|
85
|
+
// Calculate coefficient of variation for daily costs
|
|
86
|
+
const costs = usage.dailyUsage.map((d) => d.cost);
|
|
87
|
+
const mean = costs.reduce((a, b) => a + b, 0) / costs.length;
|
|
88
|
+
if (mean === 0) {
|
|
89
|
+
return "LOW";
|
|
90
|
+
}
|
|
91
|
+
const variance = costs.reduce((sum, c) => sum + Math.pow(c - mean, 2), 0) / costs.length;
|
|
92
|
+
const stdDev = Math.sqrt(variance);
|
|
93
|
+
const cv = stdDev / mean;
|
|
94
|
+
// CV thresholds for confidence
|
|
95
|
+
if (cv < 0.5)
|
|
96
|
+
return "HIGH";
|
|
97
|
+
if (cv < 1.0)
|
|
98
|
+
return "MEDIUM";
|
|
99
|
+
return "LOW";
|
|
100
|
+
}
|
|
101
|
+
function generateReasons(tier, monthlyCost, activeDays, usage) {
|
|
102
|
+
const reasons = [];
|
|
103
|
+
// Usage frequency reason
|
|
104
|
+
if (activeDays < 5) {
|
|
105
|
+
reasons.push(`Light usage (${Math.round(activeDays)} active days/month)`);
|
|
106
|
+
}
|
|
107
|
+
else if (activeDays < 15) {
|
|
108
|
+
reasons.push(`Moderate usage (${Math.round(activeDays)} active days/month)`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
reasons.push(`Heavy usage (${Math.round(activeDays)} active days/month)`);
|
|
112
|
+
}
|
|
113
|
+
// Cost-based reason
|
|
114
|
+
const breakEven = BREAK_EVEN[tier];
|
|
115
|
+
if (tier === "API Only") {
|
|
116
|
+
reasons.push(`API costs ($${monthlyCost.toFixed(0)}/mo) below subscription value`);
|
|
117
|
+
}
|
|
118
|
+
else if (monthlyCost > breakEven) {
|
|
119
|
+
reasons.push(`API costs ($${monthlyCost.toFixed(0)}/mo) exceed ${tier} break-even (~$${breakEven})`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
reasons.push(`${tier} provides buffer for usage growth`);
|
|
123
|
+
}
|
|
124
|
+
// Model mix reason
|
|
125
|
+
const opusUsage = usage.modelBreakdown
|
|
126
|
+
.filter((m) => m.model.toLowerCase().includes("opus"))
|
|
127
|
+
.reduce((sum, m) => sum + m.percentage, 0);
|
|
128
|
+
if (opusUsage > 30) {
|
|
129
|
+
reasons.push(`High Opus usage (${opusUsage.toFixed(0)}%) benefits from subscription`);
|
|
130
|
+
}
|
|
131
|
+
return reasons;
|
|
132
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
const BOX_WIDTH = 58;
|
|
3
|
+
export function renderRecommendation(usage, rec, verbose) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
// Header
|
|
6
|
+
lines.push(boxTop());
|
|
7
|
+
lines.push(boxMiddle(pc.bold("CoderPlan Tier Recommendation")));
|
|
8
|
+
lines.push(boxBottom());
|
|
9
|
+
lines.push("");
|
|
10
|
+
// Usage summary
|
|
11
|
+
lines.push(pc.bold("Your Usage") + pc.dim(` (Last ${usage.periodDays} Days)`));
|
|
12
|
+
lines.push(` Sessions: ${usage.sessionCount}`);
|
|
13
|
+
lines.push(` Active Days: ${usage.activeDays}/${usage.periodDays}`);
|
|
14
|
+
lines.push(` Total Tokens: ${formatTokens(usage.totalInputTokens + usage.totalOutputTokens)}`);
|
|
15
|
+
lines.push(` API Cost: $${usage.totalCost.toFixed(2)}`);
|
|
16
|
+
// Model breakdown
|
|
17
|
+
if (usage.modelBreakdown.length > 0) {
|
|
18
|
+
const modelStr = usage.modelBreakdown
|
|
19
|
+
.slice(0, 3)
|
|
20
|
+
.map((m) => `${simplifyModelName(m.model)} (${m.percentage.toFixed(0)}%)`)
|
|
21
|
+
.join(", ");
|
|
22
|
+
lines.push(` Primary Model: ${modelStr}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("");
|
|
25
|
+
// Recommendation
|
|
26
|
+
const tierColor = getTierColor(rec.tier);
|
|
27
|
+
lines.push(pc.bold("Recommendation: ") + tierColor(rec.tier) + pc.dim(` ($${getTierCost(rec.tier)}/month)`));
|
|
28
|
+
const confColor = getConfidenceColor(rec.confidence);
|
|
29
|
+
lines.push(` Confidence: ${confColor(rec.confidence)} ${pc.dim(getConfidenceNote(rec.confidence))}`);
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push(" Reasoning:");
|
|
32
|
+
for (const reason of rec.reasons) {
|
|
33
|
+
lines.push(` ${pc.dim("-")} ${reason}`);
|
|
34
|
+
}
|
|
35
|
+
if (rec.savings > 0) {
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push(` Projected Savings: ${pc.green(`~$${rec.savings.toFixed(0)}/month`)} vs API-only`);
|
|
38
|
+
}
|
|
39
|
+
lines.push("");
|
|
40
|
+
// Tier comparison table
|
|
41
|
+
lines.push(boxTop());
|
|
42
|
+
lines.push(boxMiddle("Tier Comparison"));
|
|
43
|
+
lines.push(boxSeparator());
|
|
44
|
+
for (const cost of rec.projectedCosts) {
|
|
45
|
+
const marker = cost.isRecommended ? pc.cyan(" <--") : "";
|
|
46
|
+
const costStr = `$${cost.monthlyCost.toFixed(0)}/month`;
|
|
47
|
+
const detail = cost.tier === "API Only"
|
|
48
|
+
? "(current spend)"
|
|
49
|
+
: cost.overflowCost > 0
|
|
50
|
+
? `($${cost.subscriptionCost} + ~$${cost.overflowCost.toFixed(0)} overflow)`
|
|
51
|
+
: "(no overflow expected)";
|
|
52
|
+
const line = `${padRight(cost.tier, 12)} ${padRight(costStr, 12)} ${pc.dim(detail)}${marker}`;
|
|
53
|
+
lines.push(boxContent(line));
|
|
54
|
+
}
|
|
55
|
+
lines.push(boxBottom());
|
|
56
|
+
// Verbose: show detailed metrics
|
|
57
|
+
if (verbose) {
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push(pc.bold("Detailed Metrics"));
|
|
60
|
+
lines.push(` Input Tokens: ${formatTokens(usage.totalInputTokens)}`);
|
|
61
|
+
lines.push(` Output Tokens: ${formatTokens(usage.totalOutputTokens)}`);
|
|
62
|
+
lines.push(` Cache Creation: ${formatTokens(usage.totalCacheCreationTokens)}`);
|
|
63
|
+
lines.push(` Cache Read: ${formatTokens(usage.totalCacheReadTokens)}`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push(pc.bold("Model Breakdown"));
|
|
66
|
+
for (const model of usage.modelBreakdown) {
|
|
67
|
+
lines.push(` ${padRight(simplifyModelName(model.model), 20)} $${model.cost.toFixed(2)} (${model.percentage.toFixed(1)}%)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
export function renderJson(usage, rec) {
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
usage: {
|
|
75
|
+
periodDays: usage.periodDays,
|
|
76
|
+
activeDays: usage.activeDays,
|
|
77
|
+
sessionCount: usage.sessionCount,
|
|
78
|
+
totalCost: usage.totalCost,
|
|
79
|
+
totalTokens: usage.totalInputTokens + usage.totalOutputTokens,
|
|
80
|
+
modelBreakdown: usage.modelBreakdown,
|
|
81
|
+
},
|
|
82
|
+
recommendation: {
|
|
83
|
+
tier: rec.tier,
|
|
84
|
+
confidence: rec.confidence,
|
|
85
|
+
reasons: rec.reasons,
|
|
86
|
+
currentMonthlyCost: rec.currentMonthlyCost,
|
|
87
|
+
projectedSavings: rec.savings,
|
|
88
|
+
tierCosts: rec.projectedCosts,
|
|
89
|
+
},
|
|
90
|
+
}, null, 2);
|
|
91
|
+
}
|
|
92
|
+
// Box drawing helpers
|
|
93
|
+
function boxTop() {
|
|
94
|
+
return pc.dim("╭" + "─".repeat(BOX_WIDTH) + "╮");
|
|
95
|
+
}
|
|
96
|
+
function boxBottom() {
|
|
97
|
+
return pc.dim("╰" + "─".repeat(BOX_WIDTH) + "╯");
|
|
98
|
+
}
|
|
99
|
+
function boxSeparator() {
|
|
100
|
+
return pc.dim("├" + "─".repeat(BOX_WIDTH) + "┤");
|
|
101
|
+
}
|
|
102
|
+
function boxMiddle(text) {
|
|
103
|
+
const stripped = stripAnsi(text);
|
|
104
|
+
const padding = Math.max(0, BOX_WIDTH - stripped.length);
|
|
105
|
+
const left = Math.floor(padding / 2);
|
|
106
|
+
const right = padding - left;
|
|
107
|
+
return pc.dim("│") + " ".repeat(left) + text + " ".repeat(right) + pc.dim("│");
|
|
108
|
+
}
|
|
109
|
+
function boxContent(text) {
|
|
110
|
+
const stripped = stripAnsi(text);
|
|
111
|
+
const padding = Math.max(0, BOX_WIDTH - stripped.length - 1);
|
|
112
|
+
return pc.dim("│") + " " + text + " ".repeat(padding) + pc.dim("│");
|
|
113
|
+
}
|
|
114
|
+
function stripAnsi(str) {
|
|
115
|
+
// eslint-disable-next-line no-control-regex
|
|
116
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
117
|
+
}
|
|
118
|
+
function padRight(str, len) {
|
|
119
|
+
return str + " ".repeat(Math.max(0, len - str.length));
|
|
120
|
+
}
|
|
121
|
+
function formatTokens(tokens) {
|
|
122
|
+
if (tokens >= 1_000_000) {
|
|
123
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
124
|
+
}
|
|
125
|
+
if (tokens >= 1_000) {
|
|
126
|
+
return `${(tokens / 1_000).toFixed(1)}K`;
|
|
127
|
+
}
|
|
128
|
+
return tokens.toString();
|
|
129
|
+
}
|
|
130
|
+
function simplifyModelName(model) {
|
|
131
|
+
const lower = model.toLowerCase();
|
|
132
|
+
if (lower.includes("opus"))
|
|
133
|
+
return "Opus";
|
|
134
|
+
if (lower.includes("sonnet"))
|
|
135
|
+
return "Sonnet";
|
|
136
|
+
if (lower.includes("haiku"))
|
|
137
|
+
return "Haiku";
|
|
138
|
+
return model.split("-").slice(0, 2).join(" ");
|
|
139
|
+
}
|
|
140
|
+
function getTierColor(tier) {
|
|
141
|
+
switch (tier) {
|
|
142
|
+
case "API Only":
|
|
143
|
+
return pc.dim;
|
|
144
|
+
case "Pro":
|
|
145
|
+
return pc.blue;
|
|
146
|
+
case "Max 5x":
|
|
147
|
+
return pc.cyan;
|
|
148
|
+
case "Max 20x":
|
|
149
|
+
return pc.magenta;
|
|
150
|
+
default:
|
|
151
|
+
return (s) => s;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function getTierCost(tier) {
|
|
155
|
+
switch (tier) {
|
|
156
|
+
case "Pro":
|
|
157
|
+
return 20;
|
|
158
|
+
case "Max 5x":
|
|
159
|
+
return 100;
|
|
160
|
+
case "Max 20x":
|
|
161
|
+
return 200;
|
|
162
|
+
default:
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function getConfidenceColor(confidence) {
|
|
167
|
+
switch (confidence) {
|
|
168
|
+
case "HIGH":
|
|
169
|
+
return pc.green;
|
|
170
|
+
case "MEDIUM":
|
|
171
|
+
return pc.yellow;
|
|
172
|
+
case "LOW":
|
|
173
|
+
return pc.red;
|
|
174
|
+
default:
|
|
175
|
+
return (s) => s;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function getConfidenceNote(confidence) {
|
|
179
|
+
switch (confidence) {
|
|
180
|
+
case "HIGH":
|
|
181
|
+
return "(consistent usage pattern)";
|
|
182
|
+
case "MEDIUM":
|
|
183
|
+
return "(some variation in usage)";
|
|
184
|
+
case "LOW":
|
|
185
|
+
return "(highly variable usage)";
|
|
186
|
+
default:
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface UsageData {
|
|
2
|
+
totalCost: number;
|
|
3
|
+
totalInputTokens: number;
|
|
4
|
+
totalOutputTokens: number;
|
|
5
|
+
totalCacheCreationTokens: number;
|
|
6
|
+
totalCacheReadTokens: number;
|
|
7
|
+
sessionCount: number;
|
|
8
|
+
activeDays: number;
|
|
9
|
+
periodDays: number;
|
|
10
|
+
modelBreakdown: ModelUsage[];
|
|
11
|
+
dailyUsage: DailyUsage[];
|
|
12
|
+
}
|
|
13
|
+
export interface ModelUsage {
|
|
14
|
+
model: string;
|
|
15
|
+
cost: number;
|
|
16
|
+
inputTokens: number;
|
|
17
|
+
outputTokens: number;
|
|
18
|
+
percentage: number;
|
|
19
|
+
}
|
|
20
|
+
export interface DailyUsage {
|
|
21
|
+
date: string;
|
|
22
|
+
cost: number;
|
|
23
|
+
sessions: number;
|
|
24
|
+
}
|
|
25
|
+
export type Tier = "API Only" | "Pro" | "Max 5x" | "Max 20x";
|
|
26
|
+
export type Confidence = "HIGH" | "MEDIUM" | "LOW";
|
|
27
|
+
export interface Recommendation {
|
|
28
|
+
tier: Tier;
|
|
29
|
+
confidence: Confidence;
|
|
30
|
+
reasons: string[];
|
|
31
|
+
projectedCosts: TierCost[];
|
|
32
|
+
currentMonthlyCost: number;
|
|
33
|
+
savings: number;
|
|
34
|
+
}
|
|
35
|
+
export interface TierCost {
|
|
36
|
+
tier: Tier;
|
|
37
|
+
monthlyCost: number;
|
|
38
|
+
subscriptionCost: number;
|
|
39
|
+
overflowCost: number;
|
|
40
|
+
isRecommended: boolean;
|
|
41
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coderplan-tier",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Analyze Claude Code usage and get tier recommendations",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"coderplan-tier": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"usage",
|
|
23
|
+
"pricing",
|
|
24
|
+
"tier"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"commander": "^12.0.0",
|
|
30
|
+
"picocolors": "^1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"tsx": "^4.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|