meter-ai 0.4.0 → 0.4.1
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/package.json +1 -1
- package/src/hooks/statusline.js +72 -4
package/package.json
CHANGED
package/src/hooks/statusline.js
CHANGED
|
@@ -2,22 +2,79 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* meter — Statusline command for Claude Code
|
|
4
4
|
*
|
|
5
|
-
* Shows
|
|
6
|
-
* 1.
|
|
7
|
-
* 2. Session actuals (
|
|
5
|
+
* Shows:
|
|
6
|
+
* 1. Estimation for the current prompt
|
|
7
|
+
* 2. Session actuals (total spend, prompt count, last cost)
|
|
8
|
+
* 3. Conservative model nudge (only when clearly appropriate)
|
|
8
9
|
*
|
|
9
|
-
* Output
|
|
10
|
+
* Output: meter ~$0.09 medium | session $0.47 (5) | last $0.12 | sonnet could save ~$0.07
|
|
10
11
|
*/
|
|
11
12
|
const fs = require('fs');
|
|
12
13
|
const path = require('path');
|
|
13
14
|
const os = require('os');
|
|
14
15
|
|
|
15
16
|
const CACHE_DIR = path.join(os.homedir(), '.meter', 'cache');
|
|
17
|
+
const CONFIG_FILE = path.join(os.homedir(), '.meter', 'config.json');
|
|
16
18
|
const ESTIMATE_FILE = path.join(CACHE_DIR, 'latest-estimate.json');
|
|
17
19
|
const SESSION_FILE = path.join(CACHE_DIR, 'session-costs.json');
|
|
18
20
|
|
|
21
|
+
// Pricing per million tokens (output, which dominates cost)
|
|
22
|
+
const MODEL_COST_RATIO = {
|
|
23
|
+
'opus': { output: 75 },
|
|
24
|
+
'sonnet': { output: 15 },
|
|
25
|
+
'haiku': { output: 1.25 },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Conservative model nudge rules:
|
|
30
|
+
* - Only nudge ONE tier down (never skip tiers)
|
|
31
|
+
* - Only for low/medium complexity (never heavy/critical)
|
|
32
|
+
* - Only when heuristic confidence was high
|
|
33
|
+
* - Opus on low task → suggest Sonnet
|
|
34
|
+
* - Opus on medium task → suggest Sonnet
|
|
35
|
+
* - Sonnet on low task → suggest Haiku
|
|
36
|
+
* - Everything else → no nudge
|
|
37
|
+
*/
|
|
38
|
+
function getModelNudge(complexity, currentModel, lastCost) {
|
|
39
|
+
if (!complexity || !currentModel || !lastCost || lastCost <= 0) return null;
|
|
40
|
+
|
|
41
|
+
const model = currentModel.toLowerCase();
|
|
42
|
+
const isOpus = model.includes('opus');
|
|
43
|
+
const isSonnet = model.includes('sonnet');
|
|
44
|
+
|
|
45
|
+
if (complexity === 'heavy' || complexity === 'critical') return null;
|
|
46
|
+
|
|
47
|
+
if (isOpus && (complexity === 'low' || complexity === 'medium')) {
|
|
48
|
+
// Opus → Sonnet: output cost ratio is 75/15 = 5x
|
|
49
|
+
const sonnetCost = lastCost / 5;
|
|
50
|
+
const savings = lastCost - sonnetCost;
|
|
51
|
+
if (savings >= 0.01) {
|
|
52
|
+
return `sonnet: ~$${sonnetCost.toFixed(2)} (save $${savings.toFixed(2)})`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isSonnet && complexity === 'low') {
|
|
57
|
+
// Sonnet → Haiku: output cost ratio is 15/1.25 = 12x
|
|
58
|
+
const haikuCost = lastCost / 12;
|
|
59
|
+
const savings = lastCost - haikuCost;
|
|
60
|
+
if (savings >= 0.01) {
|
|
61
|
+
return `haiku: ~$${haikuCost.toFixed(2)} (save $${savings.toFixed(2)})`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
19
68
|
try {
|
|
20
69
|
const parts = [];
|
|
70
|
+
let complexity = null;
|
|
71
|
+
let currentModel = null;
|
|
72
|
+
|
|
73
|
+
// Read config for current model
|
|
74
|
+
try {
|
|
75
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
76
|
+
currentModel = config.models?.claude_chain?.[0] || null;
|
|
77
|
+
} catch {}
|
|
21
78
|
|
|
22
79
|
// Part 1: Latest estimation
|
|
23
80
|
try {
|
|
@@ -25,10 +82,12 @@ try {
|
|
|
25
82
|
const age = Date.now() - (est.timestamp || 0);
|
|
26
83
|
if (age < 600_000) {
|
|
27
84
|
parts.push(`~$${est.cost.toFixed(2)} ${est.complexity}`);
|
|
85
|
+
complexity = est.complexity;
|
|
28
86
|
}
|
|
29
87
|
} catch {}
|
|
30
88
|
|
|
31
89
|
// Part 2: Session actuals
|
|
90
|
+
let lastCost = 0;
|
|
32
91
|
try {
|
|
33
92
|
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
34
93
|
const age = Date.now() - (session.last_activity || 0);
|
|
@@ -36,10 +95,19 @@ try {
|
|
|
36
95
|
parts.push(`session $${session.total_cost.toFixed(2)} (${session.prompts})`);
|
|
37
96
|
if (session.last_cost > 0) {
|
|
38
97
|
parts.push(`last $${session.last_cost.toFixed(2)}`);
|
|
98
|
+
lastCost = session.last_cost;
|
|
39
99
|
}
|
|
40
100
|
}
|
|
41
101
|
} catch {}
|
|
42
102
|
|
|
103
|
+
// Part 3: Model nudge (conservative, only when appropriate)
|
|
104
|
+
if (complexity && currentModel && lastCost > 0) {
|
|
105
|
+
const nudge = getModelNudge(complexity, currentModel, lastCost);
|
|
106
|
+
if (nudge) {
|
|
107
|
+
parts.push(nudge);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
43
111
|
if (parts.length === 0) {
|
|
44
112
|
process.stdout.write('meter: ready');
|
|
45
113
|
} else {
|