coderplan-tier 0.1.0 → 0.2.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 +24 -18
- package/dist/collector.js +101 -34
- package/dist/renderer.js +17 -8
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,43 +23,49 @@ Developers need guidance on which Claude Code pricing tier best fits their usage
|
|
|
23
23
|
|
|
24
24
|
## How It Works
|
|
25
25
|
|
|
26
|
-
1. Reads
|
|
27
|
-
|
|
26
|
+
1. Reads usage data from both:
|
|
27
|
+
- **Claude Code** via [ccusage](https://github.com/ryoppippi/ccusage)
|
|
28
|
+
- **OpenAI Codex** via [@ccusage/codex](https://www.npmjs.com/package/@ccusage/codex)
|
|
29
|
+
2. Combines and analyzes usage patterns (sessions, tokens, costs, model mix)
|
|
28
30
|
3. Calculates break-even points for each tier
|
|
29
31
|
4. Recommends the tier that minimizes your total cost
|
|
30
32
|
|
|
33
|
+
This is especially useful for developers transitioning from Codex to Claude - see your combined costs and what Claude tier would cover your usage.
|
|
34
|
+
|
|
31
35
|
## Example Output
|
|
32
36
|
|
|
33
37
|
```
|
|
34
|
-
|
|
35
|
-
│
|
|
36
|
-
|
|
38
|
+
╭──────────────────────────────────────────────────────────╮
|
|
39
|
+
│ CoderPlan Tier Recommendation │
|
|
40
|
+
╰──────────────────────────────────────────────────────────╯
|
|
37
41
|
|
|
38
42
|
Your Usage (Last 30 Days)
|
|
39
43
|
Sessions: 47
|
|
40
44
|
Active Days: 22/30
|
|
41
45
|
Total Tokens: 2.4M
|
|
42
|
-
API Cost: $
|
|
43
|
-
|
|
46
|
+
API Cost: $127.32
|
|
47
|
+
├─ Claude: $67.32 (53%)
|
|
48
|
+
└─ Codex: $60.00 (47%)
|
|
49
|
+
Primary Model: Claude Opus (45%), Codex gpt-5 (40%), Claude Sonnet (15%)
|
|
44
50
|
|
|
45
51
|
Recommendation: Max 5x ($100/month)
|
|
46
52
|
Confidence: HIGH (consistent daily usage pattern)
|
|
47
53
|
|
|
48
54
|
Reasoning:
|
|
49
|
-
-
|
|
50
|
-
-
|
|
55
|
+
- Heavy usage (22 active days/month)
|
|
56
|
+
- API costs ($127/mo) exceed Pro break-even (~$25)
|
|
51
57
|
- Max 5x limits would cover your typical usage
|
|
52
58
|
|
|
53
|
-
Projected Savings: ~$
|
|
59
|
+
Projected Savings: ~$65/month vs API-only
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
│ Tier Comparison
|
|
57
|
-
|
|
58
|
-
│ API Only
|
|
59
|
-
│ Pro
|
|
60
|
-
│ Max 5x
|
|
61
|
-
│ Max 20x
|
|
62
|
-
|
|
61
|
+
╭──────────────────────────────────────────────────────────╮
|
|
62
|
+
│ Tier Comparison │
|
|
63
|
+
├──────────────────────────────────────────────────────────┤
|
|
64
|
+
│ API Only $127/month (current spend) │
|
|
65
|
+
│ Pro $58/month ($20 + ~$38 overflow) │
|
|
66
|
+
│ Max 5x $113/month ($100 + ~$13 overflow) <-- │
|
|
67
|
+
│ Max 20x $203/month ($200 + ~$3 overflow) │
|
|
68
|
+
╰──────────────────────────────────────────────────────────╯
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
## CLI Options
|
package/dist/collector.js
CHANGED
|
@@ -1,32 +1,49 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
export async function getUsageData(days) {
|
|
3
3
|
const since = formatDateYYYYMMDD(new Date(Date.now() - days * 24 * 60 * 60 * 1000));
|
|
4
|
-
//
|
|
5
|
-
const
|
|
6
|
-
|
|
4
|
+
// Fetch both Claude and Codex usage in parallel
|
|
5
|
+
const [claudeData, codexData] = await Promise.all([
|
|
6
|
+
fetchClaudeUsage(since),
|
|
7
|
+
fetchCodexUsage(since),
|
|
8
|
+
]);
|
|
9
|
+
// Aggregate totals
|
|
7
10
|
let totalCost = 0;
|
|
8
11
|
let totalInputTokens = 0;
|
|
9
12
|
let totalOutputTokens = 0;
|
|
10
13
|
let totalCacheCreationTokens = 0;
|
|
11
14
|
let totalCacheReadTokens = 0;
|
|
15
|
+
let claudeCost = 0;
|
|
16
|
+
let codexCost = 0;
|
|
12
17
|
const modelMap = new Map();
|
|
13
|
-
|
|
18
|
+
const dailyMap = new Map();
|
|
19
|
+
// Process Claude data
|
|
20
|
+
for (const day of claudeData.daily) {
|
|
14
21
|
totalCost += day.totalCost;
|
|
22
|
+
claudeCost += day.totalCost;
|
|
15
23
|
totalInputTokens += day.inputTokens;
|
|
16
24
|
totalOutputTokens += day.outputTokens;
|
|
17
|
-
totalCacheCreationTokens += day.cacheCreationTokens;
|
|
18
|
-
totalCacheReadTokens += day.cacheReadTokens;
|
|
25
|
+
totalCacheCreationTokens += day.cacheCreationTokens ?? 0;
|
|
26
|
+
totalCacheReadTokens += day.cacheReadTokens ?? 0;
|
|
27
|
+
// Track daily usage
|
|
28
|
+
const existing = dailyMap.get(day.date);
|
|
29
|
+
if (existing) {
|
|
30
|
+
existing.cost += day.totalCost;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
dailyMap.set(day.date, { date: day.date, cost: day.totalCost, sessions: 1 });
|
|
34
|
+
}
|
|
19
35
|
// Aggregate model usage
|
|
20
|
-
for (const model of day.modelBreakdowns) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
for (const model of day.modelBreakdowns ?? []) {
|
|
37
|
+
const modelName = `Claude ${simplifyModelName(model.modelName)}`;
|
|
38
|
+
const existingModel = modelMap.get(modelName);
|
|
39
|
+
if (existingModel) {
|
|
40
|
+
existingModel.cost += model.cost;
|
|
41
|
+
existingModel.inputTokens += model.inputTokens;
|
|
42
|
+
existingModel.outputTokens += model.outputTokens;
|
|
26
43
|
}
|
|
27
44
|
else {
|
|
28
|
-
modelMap.set(
|
|
29
|
-
model:
|
|
45
|
+
modelMap.set(modelName, {
|
|
46
|
+
model: modelName,
|
|
30
47
|
cost: model.cost,
|
|
31
48
|
inputTokens: model.inputTokens,
|
|
32
49
|
outputTokens: model.outputTokens,
|
|
@@ -35,8 +52,49 @@ export async function getUsageData(days) {
|
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
}
|
|
38
|
-
|
|
39
|
-
const
|
|
55
|
+
// Process Codex data
|
|
56
|
+
for (const day of codexData.daily) {
|
|
57
|
+
const dayCost = day.costUSD ?? 0;
|
|
58
|
+
totalCost += dayCost;
|
|
59
|
+
codexCost += dayCost;
|
|
60
|
+
totalInputTokens += day.inputTokens;
|
|
61
|
+
totalOutputTokens += day.outputTokens;
|
|
62
|
+
// Track daily usage
|
|
63
|
+
const existing = dailyMap.get(day.date);
|
|
64
|
+
if (existing) {
|
|
65
|
+
existing.cost += dayCost;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
dailyMap.set(day.date, { date: day.date, cost: dayCost, sessions: 1 });
|
|
69
|
+
}
|
|
70
|
+
// Aggregate model usage from models object
|
|
71
|
+
if (day.models) {
|
|
72
|
+
for (const [modelName, modelData] of Object.entries(day.models)) {
|
|
73
|
+
const displayName = `Codex ${modelName}`;
|
|
74
|
+
const existingModel = modelMap.get(displayName);
|
|
75
|
+
// Estimate cost per model proportionally
|
|
76
|
+
const modelTokens = modelData.inputTokens + modelData.outputTokens;
|
|
77
|
+
const dayTotalTokens = day.inputTokens + day.outputTokens;
|
|
78
|
+
const modelCost = dayTotalTokens > 0 ? (modelTokens / dayTotalTokens) * dayCost : 0;
|
|
79
|
+
if (existingModel) {
|
|
80
|
+
existingModel.cost += modelCost;
|
|
81
|
+
existingModel.inputTokens += modelData.inputTokens;
|
|
82
|
+
existingModel.outputTokens += modelData.outputTokens;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
modelMap.set(displayName, {
|
|
86
|
+
model: displayName,
|
|
87
|
+
cost: modelCost,
|
|
88
|
+
inputTokens: modelData.inputTokens,
|
|
89
|
+
outputTokens: modelData.outputTokens,
|
|
90
|
+
percentage: 0,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const activeDays = Array.from(dailyMap.values()).filter((d) => d.cost > 0).length;
|
|
97
|
+
const sessionCount = dailyMap.size;
|
|
40
98
|
// Calculate percentages
|
|
41
99
|
const modelBreakdown = Array.from(modelMap.values()).map((m) => ({
|
|
42
100
|
...m,
|
|
@@ -44,11 +102,7 @@ export async function getUsageData(days) {
|
|
|
44
102
|
}));
|
|
45
103
|
// Sort by cost descending
|
|
46
104
|
modelBreakdown.sort((a, b) => b.cost - a.cost);
|
|
47
|
-
const dailyUsage =
|
|
48
|
-
date: d.date,
|
|
49
|
-
cost: d.totalCost,
|
|
50
|
-
sessions: 1,
|
|
51
|
-
}));
|
|
105
|
+
const dailyUsage = Array.from(dailyMap.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
52
106
|
return {
|
|
53
107
|
totalCost,
|
|
54
108
|
totalInputTokens,
|
|
@@ -60,37 +114,40 @@ export async function getUsageData(days) {
|
|
|
60
114
|
periodDays: days,
|
|
61
115
|
modelBreakdown,
|
|
62
116
|
dailyUsage,
|
|
117
|
+
claudeCost,
|
|
118
|
+
codexCost,
|
|
63
119
|
};
|
|
64
120
|
}
|
|
65
|
-
function
|
|
121
|
+
async function fetchClaudeUsage(since) {
|
|
122
|
+
return runCommand(`npx ccusage@latest daily --json --since ${since}`, { daily: [] });
|
|
123
|
+
}
|
|
124
|
+
async function fetchCodexUsage(since) {
|
|
125
|
+
return runCommand(`npx @ccusage/codex@latest daily --json --since ${since}`, { daily: [] });
|
|
126
|
+
}
|
|
127
|
+
function runCommand(command, fallback) {
|
|
66
128
|
try {
|
|
67
|
-
const output = execSync(
|
|
129
|
+
const output = execSync(command, {
|
|
68
130
|
encoding: "utf-8",
|
|
69
131
|
stdio: ["pipe", "pipe", "pipe"],
|
|
70
|
-
timeout:
|
|
132
|
+
timeout: 60000,
|
|
71
133
|
});
|
|
72
134
|
return JSON.parse(output);
|
|
73
135
|
}
|
|
74
136
|
catch (error) {
|
|
137
|
+
// Try to parse stdout even on error (some tools exit non-zero with valid output)
|
|
75
138
|
if (error instanceof Error && "stdout" in error) {
|
|
76
139
|
const stdout = error.stdout;
|
|
77
|
-
|
|
78
|
-
if (stdout) {
|
|
140
|
+
if (stdout?.trim()) {
|
|
79
141
|
try {
|
|
80
142
|
return JSON.parse(stdout);
|
|
81
143
|
}
|
|
82
144
|
catch {
|
|
83
|
-
// Fall through
|
|
145
|
+
// Fall through
|
|
84
146
|
}
|
|
85
147
|
}
|
|
86
148
|
}
|
|
87
|
-
// Return
|
|
88
|
-
|
|
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)}`);
|
|
149
|
+
// Return fallback for missing data
|
|
150
|
+
return fallback;
|
|
94
151
|
}
|
|
95
152
|
}
|
|
96
153
|
function formatDateYYYYMMDD(date) {
|
|
@@ -99,3 +156,13 @@ function formatDateYYYYMMDD(date) {
|
|
|
99
156
|
const day = String(date.getDate()).padStart(2, "0");
|
|
100
157
|
return `${year}${month}${day}`;
|
|
101
158
|
}
|
|
159
|
+
function simplifyModelName(model) {
|
|
160
|
+
const lower = model.toLowerCase();
|
|
161
|
+
if (lower.includes("opus"))
|
|
162
|
+
return "Opus";
|
|
163
|
+
if (lower.includes("sonnet"))
|
|
164
|
+
return "Sonnet";
|
|
165
|
+
if (lower.includes("haiku"))
|
|
166
|
+
return "Haiku";
|
|
167
|
+
return model.split("-").slice(0, 2).join(" ");
|
|
168
|
+
}
|
package/dist/renderer.js
CHANGED
|
@@ -13,6 +13,14 @@ export function renderRecommendation(usage, rec, verbose) {
|
|
|
13
13
|
lines.push(` Active Days: ${usage.activeDays}/${usage.periodDays}`);
|
|
14
14
|
lines.push(` Total Tokens: ${formatTokens(usage.totalInputTokens + usage.totalOutputTokens)}`);
|
|
15
15
|
lines.push(` API Cost: $${usage.totalCost.toFixed(2)}`);
|
|
16
|
+
// Show Claude/Codex breakdown if both have usage
|
|
17
|
+
if (usage.claudeCost > 0 && usage.codexCost > 0) {
|
|
18
|
+
lines.push(` ${pc.dim("├─")} Claude: $${usage.claudeCost.toFixed(2)} (${((usage.claudeCost / usage.totalCost) * 100).toFixed(0)}%)`);
|
|
19
|
+
lines.push(` ${pc.dim("└─")} Codex: $${usage.codexCost.toFixed(2)} (${((usage.codexCost / usage.totalCost) * 100).toFixed(0)}%)`);
|
|
20
|
+
}
|
|
21
|
+
else if (usage.codexCost > 0) {
|
|
22
|
+
lines.push(` ${pc.dim("└─")} Codex only`);
|
|
23
|
+
}
|
|
16
24
|
// Model breakdown
|
|
17
25
|
if (usage.modelBreakdown.length > 0) {
|
|
18
26
|
const modelStr = usage.modelBreakdown
|
|
@@ -76,6 +84,8 @@ export function renderJson(usage, rec) {
|
|
|
76
84
|
activeDays: usage.activeDays,
|
|
77
85
|
sessionCount: usage.sessionCount,
|
|
78
86
|
totalCost: usage.totalCost,
|
|
87
|
+
claudeCost: usage.claudeCost,
|
|
88
|
+
codexCost: usage.codexCost,
|
|
79
89
|
totalTokens: usage.totalInputTokens + usage.totalOutputTokens,
|
|
80
90
|
modelBreakdown: usage.modelBreakdown,
|
|
81
91
|
},
|
|
@@ -128,14 +138,13 @@ function formatTokens(tokens) {
|
|
|
128
138
|
return tokens.toString();
|
|
129
139
|
}
|
|
130
140
|
function simplifyModelName(model) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return model.split("-").slice(0, 2).join(" ");
|
|
141
|
+
// Model names already have "Claude" or "Codex" prefix from collector
|
|
142
|
+
// Just clean up for display
|
|
143
|
+
return model
|
|
144
|
+
.replace("claude-", "")
|
|
145
|
+
.replace("-20251101", "")
|
|
146
|
+
.replace("-20251001", "")
|
|
147
|
+
.replace("-20250929", "");
|
|
139
148
|
}
|
|
140
149
|
function getTierColor(tier) {
|
|
141
150
|
switch (tier) {
|
package/dist/types.d.ts
CHANGED