meter-ai 0.3.0 → 0.4.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 +69 -32
- package/dist/commands/init.js +12 -1
- package/dist/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/init.ts +16 -1
- package/src/hooks/on-stop.js +78 -0
- package/src/hooks/statusline.js +33 -19
package/README.md
CHANGED
|
@@ -2,16 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
**Intelligent CLI wrapper for Claude Code** — pre-task cost estimation, live status bar, budget protection.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Why I Built This
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
I'm a student. My budget is tight, but my urge to build things is not. I think about projects in my sleep. I wake up and start coding. I use Claude Code every single day to build tools that empower myself and the community around me.
|
|
8
|
+
|
|
9
|
+
But here's the thing — I had no idea what any of it cost. I'd run a prompt, Claude would do its thing, and at the end of the month I'd stare at a bill wondering which Tuesday afternoon wiped out half my budget. Or worse, I'd hit my 5-hour rate limit wall right in the middle of something important and have to stop dead.
|
|
10
|
+
|
|
11
|
+
I built meter so I could see what my actions actually cost. Not after the fact — but as I work. Every prompt, every task, every session. Because when you're a student building on a budget, awareness is the difference between building sustainably and burning out your credits in a week.
|
|
12
|
+
|
|
13
|
+
If you're in the same boat — stretching every dollar, every token, every rate limit window — this is for you.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What It Does
|
|
18
|
+
|
|
19
|
+
meter sits transparently between you and Claude Code. You keep typing `claude` exactly as before. meter adds:
|
|
20
|
+
|
|
21
|
+
- **Per-prompt estimation** — see the estimated cost of each prompt in Claude Code's statusline
|
|
8
22
|
- **Live status bar** — ambient cost/usage ticker while the agent works
|
|
9
|
-
- **Budget protection** —
|
|
23
|
+
- **Budget protection** — notifications when thresholds are hit, with model switching
|
|
24
|
+
- **Session history** — track what you spent, on what, and learn your patterns
|
|
10
25
|
- **Works for both API users and plan subscribers**
|
|
11
26
|
|
|
12
27
|
```
|
|
13
|
-
|
|
14
|
-
◆ meter claude-opus ~$0.38 heavy ████████████░░░░ 68% 5hr window | $0.12 elapsed
|
|
28
|
+
◆ meter claude-opus ~$0.38 heavy │ 68% 5hr window reset in 2h 14m
|
|
15
29
|
```
|
|
16
30
|
|
|
17
31
|
## Install
|
|
@@ -22,19 +36,28 @@ meter init
|
|
|
22
36
|
# restart your terminal
|
|
23
37
|
```
|
|
24
38
|
|
|
25
|
-
That's it. `meter init` detects your shell, finds the real `claude` binary, sets up a transparent shim, and writes your config. From
|
|
39
|
+
That's it. `meter init` detects your shell, finds the real `claude` binary, sets up a transparent shim, installs estimation hooks into Claude Code, and writes your config. From that point on, every `claude` invocation runs through meter automatically.
|
|
26
40
|
|
|
27
41
|
## How It Works
|
|
28
42
|
|
|
29
|
-
meter
|
|
43
|
+
meter integrates with Claude Code in two ways:
|
|
30
44
|
|
|
31
|
-
###
|
|
45
|
+
### 1. Claude Code Hooks (interactive sessions)
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
47
|
+
When you launch `claude` interactively, meter hooks into Claude Code's native event system:
|
|
48
|
+
|
|
49
|
+
- **`UserPromptSubmit` hook** — runs the estimation pipeline every time you submit a prompt
|
|
50
|
+
- **Statusline integration** — displays the estimate in Claude Code's own bottom bar
|
|
51
|
+
|
|
52
|
+
This means you see the cost estimation right inside Claude Code's UI, alongside your existing statusline info. No separate window, no terminal conflicts.
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
### 2. PTY Wrapper (non-interactive / one-shot commands)
|
|
55
|
+
|
|
56
|
+
When you run `claude "fix the bug"` with a prompt argument, meter wraps the process in a PTY and shows a live status bar with real-time cost tracking.
|
|
57
|
+
|
|
58
|
+
### For API users (paying per token)
|
|
59
|
+
|
|
60
|
+
meter tracks actual dollar cost. When your per-task budget threshold is hit:
|
|
38
61
|
|
|
39
62
|
```
|
|
40
63
|
⚠ 80% budget hit ($0.40/$0.50) [s]witch to sonnet [d]ismiss [c]ancel
|
|
@@ -42,31 +65,36 @@ meter tracks actual dollar cost in real time. When your per-task budget threshol
|
|
|
42
65
|
|
|
43
66
|
### For plan users (Claude Max, Pro)
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
◆ meter claude-opus ~$0.38 heavy │ ████████████░░░░ 68% 5hr window reset in 2h 14m
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
meter polls your 5-hour usage window percentage and warns before you hit the rate limit wall mid-task.
|
|
68
|
+
meter tracks your 5-hour usage window percentage and warns before you hit the rate limit wall mid-task.
|
|
50
69
|
|
|
51
|
-
## Pre-
|
|
70
|
+
## Pre-Prompt Estimation
|
|
52
71
|
|
|
53
72
|
meter estimates task cost/weight using a three-layer pipeline:
|
|
54
73
|
|
|
55
74
|
1. **Heuristics** (~50ms, free) — keyword scoring + repo size analysis
|
|
56
|
-
2. **Historical baseline** (~10ms, free) — trigram similarity against your past tasks
|
|
57
|
-
3. **LLM pre-call** (~1-2s, ~$0.0001) — only fires on ambiguous tasks
|
|
75
|
+
2. **Historical baseline** (~10ms, free) — trigram similarity against your past tasks in `~/.meter/history.db`
|
|
76
|
+
3. **LLM pre-call** (~1-2s, ~$0.0001) — only fires on ambiguous tasks, asks Haiku for a one-word complexity classification
|
|
77
|
+
|
|
78
|
+
Simple tasks never hit layer 3. The pipeline gets smarter over time as your history builds up — Layer 2 learns from your real usage patterns and replaces guesswork with data.
|
|
79
|
+
|
|
80
|
+
### Cost estimates by complexity
|
|
58
81
|
|
|
59
|
-
|
|
82
|
+
| Complexity | Estimated cost |
|
|
83
|
+
|---|---|
|
|
84
|
+
| low | ~$0.02 |
|
|
85
|
+
| medium | ~$0.09 |
|
|
86
|
+
| heavy | ~$0.38 |
|
|
87
|
+
| critical | ~$0.80 |
|
|
60
88
|
|
|
61
89
|
## Commands
|
|
62
90
|
|
|
63
91
|
```bash
|
|
64
|
-
meter init # Set up
|
|
92
|
+
meter init # Set up shim, hooks, and config
|
|
65
93
|
meter status # Show current mode, model, usage, and config
|
|
66
94
|
meter report # Weekly digest of usage and costs
|
|
67
95
|
meter history # Browse past task records
|
|
68
96
|
meter config # View and set budgets, thresholds, model chain
|
|
69
|
-
meter uninstall # Clean removal of all shims and data
|
|
97
|
+
meter uninstall # Clean removal of all shims, hooks, and data
|
|
70
98
|
```
|
|
71
99
|
|
|
72
100
|
### Configuration
|
|
@@ -93,18 +121,27 @@ All data stays local. No telemetry, no cloud, no accounts.
|
|
|
93
121
|
|
|
94
122
|
```
|
|
95
123
|
~/.meter/
|
|
96
|
-
config.json
|
|
97
|
-
history.db
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
config.json # Mode, budgets, thresholds, resolved binary path
|
|
125
|
+
history.db # SQLite: task history, costs, estimates
|
|
126
|
+
hooks/
|
|
127
|
+
on-prompt.js # Claude Code UserPromptSubmit hook
|
|
128
|
+
statusline.js # Claude Code statusline command
|
|
129
|
+
bin/claude # PATH shim
|
|
130
|
+
cache/
|
|
131
|
+
latest-estimate.json # Most recent estimation result
|
|
132
|
+
reports/ # Weekly digest snapshots
|
|
102
133
|
```
|
|
103
134
|
|
|
135
|
+
## Known Limitations
|
|
136
|
+
|
|
137
|
+
**Estimation timing in interactive sessions.** Claude Code's hook system fires the `UserPromptSubmit` event *when* the prompt is submitted, not before. This means the estimation appears in the statusline as Claude is already processing your prompt — you see the cost *during* execution, not before you press Enter. This is a platform limitation of Claude Code's hook architecture, not a meter limitation. If Anthropic adds a pre-submit hook in the future, meter will support it immediately.
|
|
138
|
+
|
|
139
|
+
**What this means in practice:** You won't get a "this will cost $X, proceed?" gate before each prompt. Instead, meter gives you ambient awareness — you see the estimated cost of each prompt as it runs, learn your patterns over time, and build intuition for what's expensive. The `meter report` and `meter history` commands help you review and learn from your spending.
|
|
140
|
+
|
|
104
141
|
## Uninstall
|
|
105
142
|
|
|
106
143
|
```bash
|
|
107
|
-
meter uninstall # Removes shim and PATH entry
|
|
144
|
+
meter uninstall # Removes shim, hooks, and PATH entry
|
|
108
145
|
npm uninstall -g meter-ai
|
|
109
146
|
```
|
|
110
147
|
|
|
@@ -113,11 +150,11 @@ npm uninstall -g meter-ai
|
|
|
113
150
|
- **v1.1** — Aider support
|
|
114
151
|
- **v1.2** — Gemini CLI support
|
|
115
152
|
- **v1.3** — Codex CLI support
|
|
116
|
-
- **v2.0** —
|
|
153
|
+
- **v2.0** — Session cost summary on exit, `meter estimate "prompt"` pre-check command, CSV/JSON export
|
|
117
154
|
|
|
118
155
|
## Support
|
|
119
156
|
|
|
120
|
-
|
|
157
|
+
I built this as a student, for students and solo devs who care about every dollar. If meter helped you understand your AI spending better, consider [buying me a coffee](https://buymeacoffee.com/catancs).
|
|
121
158
|
|
|
122
159
|
## License
|
|
123
160
|
|
package/dist/commands/init.js
CHANGED
|
@@ -65,7 +65,7 @@ async function installClaudeHooks(meterDir) {
|
|
|
65
65
|
await mkdir(hooksDir, { recursive: true });
|
|
66
66
|
// Copy hook scripts to ~/.meter/hooks/
|
|
67
67
|
const srcDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
|
|
68
|
-
for (const file of ['on-prompt.js', 'statusline.js']) {
|
|
68
|
+
for (const file of ['on-prompt.js', 'on-stop.js', 'statusline.js']) {
|
|
69
69
|
try {
|
|
70
70
|
await copyFile(join(srcDir, file), join(hooksDir, file));
|
|
71
71
|
await chmod(join(hooksDir, file), 0o755);
|
|
@@ -104,6 +104,17 @@ async function installClaudeHooks(meterDir) {
|
|
|
104
104
|
});
|
|
105
105
|
console.log('✓ Added meter estimation hook to Claude Code');
|
|
106
106
|
}
|
|
107
|
+
// Add Stop hook for post-response cost tracking
|
|
108
|
+
const meterStopCommand = `node "${join(hooksDir, 'on-stop.js')}"`;
|
|
109
|
+
if (!settings.hooks.Stop)
|
|
110
|
+
settings.hooks.Stop = [];
|
|
111
|
+
const alreadyHasStopHook = settings.hooks.Stop.some((h) => h.hooks?.some((hh) => hh.command?.includes('meter')));
|
|
112
|
+
if (!alreadyHasStopHook) {
|
|
113
|
+
settings.hooks.Stop.push({
|
|
114
|
+
hooks: [{ type: 'command', command: meterStopCommand }]
|
|
115
|
+
});
|
|
116
|
+
console.log('✓ Added meter cost tracking hook to Claude Code');
|
|
117
|
+
}
|
|
107
118
|
// Add/update statusline command
|
|
108
119
|
const meterStatuslineCommand = `node "${join(hooksDir, 'statusline.js')}"`;
|
|
109
120
|
const existingStatusLine = settings.statusLine;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -84,7 +84,7 @@ async function installClaudeHooks(meterDir: string): Promise<void> {
|
|
|
84
84
|
|
|
85
85
|
// Copy hook scripts to ~/.meter/hooks/
|
|
86
86
|
const srcDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'hooks')
|
|
87
|
-
for (const file of ['on-prompt.js', 'statusline.js']) {
|
|
87
|
+
for (const file of ['on-prompt.js', 'on-stop.js', 'statusline.js']) {
|
|
88
88
|
try {
|
|
89
89
|
await copyFile(join(srcDir, file), join(hooksDir, file))
|
|
90
90
|
await chmod(join(hooksDir, file), 0o755)
|
|
@@ -125,6 +125,21 @@ async function installClaudeHooks(meterDir: string): Promise<void> {
|
|
|
125
125
|
console.log('✓ Added meter estimation hook to Claude Code')
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Add Stop hook for post-response cost tracking
|
|
129
|
+
const meterStopCommand = `node "${join(hooksDir, 'on-stop.js')}"`
|
|
130
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = []
|
|
131
|
+
|
|
132
|
+
const alreadyHasStopHook = settings.hooks.Stop.some((h: any) =>
|
|
133
|
+
h.hooks?.some((hh: any) => hh.command?.includes('meter'))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if (!alreadyHasStopHook) {
|
|
137
|
+
settings.hooks.Stop.push({
|
|
138
|
+
hooks: [{ type: 'command', command: meterStopCommand }]
|
|
139
|
+
})
|
|
140
|
+
console.log('✓ Added meter cost tracking hook to Claude Code')
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
// Add/update statusline command
|
|
129
144
|
const meterStatuslineCommand = `node "${join(hooksDir, 'statusline.js')}"`
|
|
130
145
|
const existingStatusLine = settings.statusLine
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* meter — Stop hook
|
|
4
|
+
*
|
|
5
|
+
* Fires after Claude Code finishes responding to a prompt.
|
|
6
|
+
* Reads token usage from the response, calculates actual cost,
|
|
7
|
+
* and updates the session tracker in ~/.meter/cache/session-costs.json
|
|
8
|
+
* The statusline reads this to show actual costs.
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const METER_DIR = path.join(os.homedir(), '.meter');
|
|
15
|
+
const CACHE_DIR = path.join(METER_DIR, 'cache');
|
|
16
|
+
const SESSION_FILE = path.join(CACHE_DIR, 'session-costs.json');
|
|
17
|
+
const CONFIG_FILE = path.join(METER_DIR, 'config.json');
|
|
18
|
+
|
|
19
|
+
// Default pricing (per million tokens)
|
|
20
|
+
const PRICING = {
|
|
21
|
+
'claude-opus-4-20250514': { input: 15, output: 75 },
|
|
22
|
+
'claude-sonnet-4-20250514': { input: 3, output: 15 },
|
|
23
|
+
'claude-haiku-4-20250307': { input: 0.25, output: 1.25 },
|
|
24
|
+
'default': { input: 15, output: 75 },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const input = fs.readFileSync(0, 'utf-8').trim();
|
|
29
|
+
if (!input) process.exit(0);
|
|
30
|
+
|
|
31
|
+
let data;
|
|
32
|
+
try { data = JSON.parse(input); } catch { process.exit(0); }
|
|
33
|
+
|
|
34
|
+
// Extract token usage from stop event data
|
|
35
|
+
const tokensIn = data.usage?.input_tokens || data.input_tokens || 0;
|
|
36
|
+
const tokensOut = data.usage?.output_tokens || data.output_tokens || 0;
|
|
37
|
+
const totalTokens = tokensIn + tokensOut;
|
|
38
|
+
|
|
39
|
+
// If no token data, estimate from response length
|
|
40
|
+
const responseLen = (data.response || data.message || '').length;
|
|
41
|
+
const estimatedTokensOut = tokensOut || Math.ceil(responseLen / 4);
|
|
42
|
+
|
|
43
|
+
// Get model and pricing
|
|
44
|
+
let model = 'default';
|
|
45
|
+
try {
|
|
46
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
47
|
+
model = config.models?.claude_chain?.[0] || 'default';
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
const pricing = PRICING[model] || PRICING['default'];
|
|
51
|
+
const cost = ((tokensIn || 500) / 1_000_000) * pricing.input +
|
|
52
|
+
((estimatedTokensOut || 200) / 1_000_000) * pricing.output;
|
|
53
|
+
|
|
54
|
+
// Load or create session tracker
|
|
55
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
56
|
+
let session = { prompts: 0, total_cost: 0, last_cost: 0, heaviest_cost: 0, started_at: Date.now() };
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const existing = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
60
|
+
// If last activity was more than 30 minutes ago, start a new session
|
|
61
|
+
const age = Date.now() - (existing.last_activity || 0);
|
|
62
|
+
if (age < 1_800_000) {
|
|
63
|
+
session = existing;
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
|
|
67
|
+
// Update session
|
|
68
|
+
session.prompts += 1;
|
|
69
|
+
session.last_cost = cost;
|
|
70
|
+
session.total_cost += cost;
|
|
71
|
+
session.heaviest_cost = Math.max(session.heaviest_cost, cost);
|
|
72
|
+
session.last_activity = Date.now();
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(session));
|
|
75
|
+
} catch {
|
|
76
|
+
// Never block Claude Code
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
package/src/hooks/statusline.js
CHANGED
|
@@ -2,35 +2,49 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* meter — Statusline command for Claude Code
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Shows two things:
|
|
6
|
+
* 1. The latest estimation (from on-prompt.js) — what the current prompt is expected to cost
|
|
7
|
+
* 2. Session actuals (from on-stop.js) — what you've actually spent this session
|
|
8
|
+
*
|
|
9
|
+
* Output format: meter ~$0.09 medium | session $0.47 (5) | last $0.12
|
|
7
10
|
*/
|
|
8
11
|
const fs = require('fs');
|
|
9
12
|
const path = require('path');
|
|
10
13
|
const os = require('os');
|
|
11
14
|
|
|
12
|
-
const
|
|
15
|
+
const CACHE_DIR = path.join(os.homedir(), '.meter', 'cache');
|
|
16
|
+
const ESTIMATE_FILE = path.join(CACHE_DIR, 'latest-estimate.json');
|
|
17
|
+
const SESSION_FILE = path.join(CACHE_DIR, 'session-costs.json');
|
|
13
18
|
|
|
14
19
|
try {
|
|
15
|
-
|
|
16
|
-
process.stdout.write('meter: ready');
|
|
17
|
-
process.exit(0);
|
|
18
|
-
}
|
|
20
|
+
const parts = [];
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
// Part 1: Latest estimation
|
|
23
|
+
try {
|
|
24
|
+
const est = JSON.parse(fs.readFileSync(ESTIMATE_FILE, 'utf-8'));
|
|
25
|
+
const age = Date.now() - (est.timestamp || 0);
|
|
26
|
+
if (age < 600_000) {
|
|
27
|
+
parts.push(`~$${est.cost.toFixed(2)} ${est.complexity}`);
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
22
30
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
// Part 2: Session actuals
|
|
32
|
+
try {
|
|
33
|
+
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
34
|
+
const age = Date.now() - (session.last_activity || 0);
|
|
35
|
+
if (age < 1_800_000 && session.prompts > 0) {
|
|
36
|
+
parts.push(`session $${session.total_cost.toFixed(2)} (${session.prompts})`);
|
|
37
|
+
if (session.last_cost > 0) {
|
|
38
|
+
parts.push(`last $${session.last_cost.toFixed(2)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
if (parts.length === 0) {
|
|
44
|
+
process.stdout.write('meter: ready');
|
|
45
|
+
} else {
|
|
46
|
+
process.stdout.write('meter ' + parts.join(' | '));
|
|
47
|
+
}
|
|
34
48
|
} catch {
|
|
35
49
|
process.stdout.write('meter: ready');
|
|
36
50
|
}
|