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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meter-ai",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Intelligent CLI wrapper for Claude Code — pre-task cost estimation, live status bar, budget protection",
5
5
  "bin": {
6
6
  "meter": "dist/index.js"
@@ -2,22 +2,79 @@
2
2
  /**
3
3
  * meter — Statusline command for Claude Code
4
4
  *
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
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 format: meter ~$0.09 medium | session $0.47 (5) | last $0.12
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 {