meter-ai 0.2.0 → 0.3.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/README.md +69 -32
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +74 -3
- package/dist/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/credentials.ts +44 -0
- package/src/auth/detect.ts +24 -0
- package/src/commands/config.ts +19 -0
- package/src/commands/history.ts +16 -0
- package/src/commands/init.ts +149 -0
- package/src/commands/report.ts +27 -0
- package/src/commands/status.ts +16 -0
- package/src/commands/uninstall.ts +20 -0
- package/src/commands/wrap.ts +235 -0
- package/src/constants.ts +52 -0
- package/src/estimation/heuristics.ts +36 -0
- package/src/estimation/history-matcher.ts +43 -0
- package/src/estimation/llm-precheck.ts +27 -0
- package/src/estimation/pipeline.ts +67 -0
- package/src/hooks/on-prompt.js +92 -0
- package/src/hooks/statusline.js +36 -0
- package/src/index.ts +50 -0
- package/src/pty/resize.ts +15 -0
- package/src/pty/screen.ts +15 -0
- package/src/pty/wrapper.ts +143 -0
- package/src/shell/binary-resolver.ts +21 -0
- package/src/shell/detect.ts +33 -0
- package/src/shell/path-inject.ts +31 -0
- package/src/shell/shim-writer.ts +28 -0
- package/src/storage/config-store.ts +46 -0
- package/src/storage/db.ts +63 -0
- package/src/tracking/cost.ts +7 -0
- package/src/tracking/plan-usage.ts +57 -0
- package/src/tracking/tokens.ts +16 -0
- package/src/types.ts +73 -0
- package/src/ui/keypress.ts +27 -0
- package/src/ui/notification.ts +31 -0
- package/src/ui/statusbar.ts +74 -0
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
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAwD9D"}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkdir } from 'fs/promises';
|
|
2
|
-
import { join } from 'path';
|
|
1
|
+
import { mkdir, readFile, writeFile, copyFile, chmod } from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
import { detectShell, getShellConfigPath } from '../shell/detect.js';
|
|
4
5
|
import { resolveTrueBinary } from '../shell/binary-resolver.js';
|
|
5
6
|
import { writeShim } from '../shell/shim-writer.js';
|
|
@@ -8,7 +9,7 @@ import { writeConfig, ensureConfigDefaults } from '../storage/config-store.js';
|
|
|
8
9
|
import { detectMode } from '../auth/detect.js';
|
|
9
10
|
import { readCredentials } from '../auth/credentials.js';
|
|
10
11
|
import { resolveOrgId } from '../tracking/plan-usage.js';
|
|
11
|
-
import { CLAUDE_CREDENTIALS_PATH } from '../constants.js';
|
|
12
|
+
import { CLAUDE_CREDENTIALS_PATH, CLAUDE_SETTINGS_PATH } from '../constants.js';
|
|
12
13
|
export async function runInit(opts) {
|
|
13
14
|
const meterDir = opts.meterDir;
|
|
14
15
|
const binDir = join(meterDir, 'bin');
|
|
@@ -52,7 +53,77 @@ export async function runInit(opts) {
|
|
|
52
53
|
...(orgId ? { org_id: orgId } : {}),
|
|
53
54
|
});
|
|
54
55
|
await writeConfig(join(meterDir, 'config.json'), config);
|
|
56
|
+
// Install Claude Code hooks (skip in tests)
|
|
57
|
+
if (!opts.skipPathInjection) {
|
|
58
|
+
await installClaudeHooks(meterDir);
|
|
59
|
+
}
|
|
55
60
|
console.log(`✓ meter initialised (${mode} mode)`);
|
|
56
61
|
console.log(` Restart your terminal or run: source ~/.zshrc`);
|
|
57
62
|
}
|
|
63
|
+
async function installClaudeHooks(meterDir) {
|
|
64
|
+
const hooksDir = join(meterDir, 'hooks');
|
|
65
|
+
await mkdir(hooksDir, { recursive: true });
|
|
66
|
+
// Copy hook scripts to ~/.meter/hooks/
|
|
67
|
+
const srcDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
|
|
68
|
+
for (const file of ['on-prompt.js', 'statusline.js']) {
|
|
69
|
+
try {
|
|
70
|
+
await copyFile(join(srcDir, file), join(hooksDir, file));
|
|
71
|
+
await chmod(join(hooksDir, file), 0o755);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// If source doesn't exist (running from dist), try the dist/hooks path
|
|
75
|
+
const distDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'src', 'hooks');
|
|
76
|
+
try {
|
|
77
|
+
await copyFile(join(distDir, file), join(hooksDir, file));
|
|
78
|
+
await chmod(join(hooksDir, file), 0o755);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
console.warn(` ⚠ Could not copy ${file} — skipping hook installation`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Update Claude Code settings.json to add meter hooks
|
|
86
|
+
try {
|
|
87
|
+
let settings = {};
|
|
88
|
+
try {
|
|
89
|
+
settings = JSON.parse(await readFile(CLAUDE_SETTINGS_PATH, 'utf-8'));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// settings file doesn't exist yet — we'll create it
|
|
93
|
+
}
|
|
94
|
+
// Add UserPromptSubmit hook if not already present
|
|
95
|
+
const meterHookCommand = `node "${join(hooksDir, 'on-prompt.js')}"`;
|
|
96
|
+
if (!settings.hooks)
|
|
97
|
+
settings.hooks = {};
|
|
98
|
+
if (!settings.hooks.UserPromptSubmit)
|
|
99
|
+
settings.hooks.UserPromptSubmit = [];
|
|
100
|
+
const alreadyHasHook = settings.hooks.UserPromptSubmit.some((h) => h.hooks?.some((hh) => hh.command?.includes('meter')));
|
|
101
|
+
if (!alreadyHasHook) {
|
|
102
|
+
settings.hooks.UserPromptSubmit.push({
|
|
103
|
+
hooks: [{ type: 'command', command: meterHookCommand }]
|
|
104
|
+
});
|
|
105
|
+
console.log('✓ Added meter estimation hook to Claude Code');
|
|
106
|
+
}
|
|
107
|
+
// Add/update statusline command
|
|
108
|
+
const meterStatuslineCommand = `node "${join(hooksDir, 'statusline.js')}"`;
|
|
109
|
+
const existingStatusLine = settings.statusLine;
|
|
110
|
+
if (!existingStatusLine || existingStatusLine.command?.includes('meter')) {
|
|
111
|
+
// No existing statusline or it's ours — set it
|
|
112
|
+
settings.statusLine = { type: 'command', command: meterStatuslineCommand };
|
|
113
|
+
console.log('✓ Added meter statusline to Claude Code');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// There's an existing statusline from another tool — chain them
|
|
117
|
+
// Create a wrapper that runs both
|
|
118
|
+
const chainCommand = `${existingStatusLine.command} && echo " │ " && ${meterStatuslineCommand}`;
|
|
119
|
+
settings.statusLine = { type: 'command', command: chainCommand };
|
|
120
|
+
console.log('✓ Chained meter statusline with existing statusline');
|
|
121
|
+
}
|
|
122
|
+
await mkdir(dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
123
|
+
await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
console.warn(` ⚠ Could not update Claude Code settings: ${err}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
58
129
|
//# sourceMappingURL=init.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { watch } from 'fs'
|
|
2
|
+
import { readFile as readFileAsync } from 'fs/promises'
|
|
3
|
+
|
|
4
|
+
export interface ClaudeCredentials {
|
|
5
|
+
accessToken: string
|
|
6
|
+
refreshToken?: string
|
|
7
|
+
expiresAt?: number
|
|
8
|
+
[key: string]: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function readCredentials(path: string): Promise<ClaudeCredentials | null> {
|
|
12
|
+
async function tryRead(): Promise<ClaudeCredentials | null> {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFileAsync(path, 'utf-8')
|
|
15
|
+
return JSON.parse(raw) as ClaudeCredentials
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const first = await tryRead()
|
|
22
|
+
if (first) return first
|
|
23
|
+
|
|
24
|
+
// retry once after 200ms (handles partial writes on macOS kqueue)
|
|
25
|
+
await new Promise(r => setTimeout(r, 200))
|
|
26
|
+
return tryRead()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function watchCredentials(
|
|
30
|
+
path: string,
|
|
31
|
+
onChange: (creds: ClaudeCredentials | null) => void
|
|
32
|
+
): () => void {
|
|
33
|
+
let debounceTimer: NodeJS.Timeout | null = null
|
|
34
|
+
|
|
35
|
+
const watcher = watch(path, () => {
|
|
36
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
37
|
+
debounceTimer = setTimeout(async () => {
|
|
38
|
+
const creds = await readCredentials(path)
|
|
39
|
+
onChange(creds)
|
|
40
|
+
}, 100) // 100ms debounce to avoid partial-write reads
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return () => watcher.close()
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { access } from 'fs/promises'
|
|
2
|
+
import type { UserMode } from '../types.js'
|
|
3
|
+
|
|
4
|
+
interface DetectOptions {
|
|
5
|
+
credentialsPath: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
9
|
+
try {
|
|
10
|
+
await access(path)
|
|
11
|
+
return true
|
|
12
|
+
} catch {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function detectMode(opts: DetectOptions): Promise<UserMode | null> {
|
|
18
|
+
const hasApiKey = Boolean(process.env.ANTHROPIC_API_KEY)
|
|
19
|
+
const hasCredentials = await fileExists(opts.credentialsPath)
|
|
20
|
+
|
|
21
|
+
if (hasCredentials) return 'plan'
|
|
22
|
+
if (hasApiKey) return 'api'
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from '../storage/config-store.js'
|
|
2
|
+
import { CONFIG_PATH } from '../constants.js'
|
|
3
|
+
|
|
4
|
+
export async function runConfig(args: string[]): Promise<void> {
|
|
5
|
+
const config = await readConfig(CONFIG_PATH)
|
|
6
|
+
if (!config) { console.log('Run meter init first.'); return }
|
|
7
|
+
if (args.length === 0) { console.log(JSON.stringify(config, null, 2)); return }
|
|
8
|
+
if (args[0] === 'set' && args[1] && args[2]) {
|
|
9
|
+
const field = args[1]
|
|
10
|
+
const val = args[2]
|
|
11
|
+
if (field === 'budget') config.budget.per_task_usd = parseFloat(val)
|
|
12
|
+
else if (field === 'threshold') {
|
|
13
|
+
config.budget.threshold_pct = parseInt(val, 10)
|
|
14
|
+
config.plan.window_threshold_pct = parseInt(val, 10)
|
|
15
|
+
}
|
|
16
|
+
await writeConfig(CONFIG_PATH, config)
|
|
17
|
+
console.log(`✓ ${field} set to ${val}`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { openDb, getRecentTasks } from '../storage/db.js'
|
|
2
|
+
import { HISTORY_DB_PATH } from '../constants.js'
|
|
3
|
+
|
|
4
|
+
export async function runHistory(args: string[]): Promise<void> {
|
|
5
|
+
const limit = parseInt(args[0] ?? '30', 10)
|
|
6
|
+
const db = await openDb(HISTORY_DB_PATH)
|
|
7
|
+
const tasks = getRecentTasks(db, limit)
|
|
8
|
+
db.close()
|
|
9
|
+
if (tasks.length === 0) { console.log('No tasks recorded yet.'); return }
|
|
10
|
+
console.log(`\n◆ meter history (last ${limit})\n`)
|
|
11
|
+
for (const t of tasks) {
|
|
12
|
+
const date = new Date(t.created_at).toLocaleString()
|
|
13
|
+
const cost = t.actual_cost != null ? `$${t.actual_cost.toFixed(3)}` : t.window_pct_end != null ? `${t.window_pct_end.toFixed(0)}% window` : '—'
|
|
14
|
+
console.log(` ${date} ${t.complexity.padEnd(8)} ${cost.padStart(10)} ${t.prompt_text.slice(0, 50)}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, copyFile, chmod } from 'fs/promises'
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { detectShell, getShellConfigPath } from '../shell/detect.js'
|
|
5
|
+
import { resolveTrueBinary } from '../shell/binary-resolver.js'
|
|
6
|
+
import { writeShim } from '../shell/shim-writer.js'
|
|
7
|
+
import { injectPath, isPathAlreadyInjected } from '../shell/path-inject.js'
|
|
8
|
+
import { writeConfig, ensureConfigDefaults } from '../storage/config-store.js'
|
|
9
|
+
import { detectMode } from '../auth/detect.js'
|
|
10
|
+
import { readCredentials } from '../auth/credentials.js'
|
|
11
|
+
import { resolveOrgId } from '../tracking/plan-usage.js'
|
|
12
|
+
import { METER_BIN_DIR, CLAUDE_CREDENTIALS_PATH, CLAUDE_SETTINGS_PATH } from '../constants.js'
|
|
13
|
+
import type { UserMode } from '../types.js'
|
|
14
|
+
|
|
15
|
+
export interface InitOptions {
|
|
16
|
+
meterDir: string
|
|
17
|
+
trueBinary?: string
|
|
18
|
+
mode?: UserMode
|
|
19
|
+
skipPathInjection?: boolean
|
|
20
|
+
orgId?: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runInit(opts: InitOptions): Promise<void> {
|
|
24
|
+
const meterDir = opts.meterDir
|
|
25
|
+
const binDir = join(meterDir, 'bin')
|
|
26
|
+
await mkdir(binDir, { recursive: true })
|
|
27
|
+
await mkdir(join(meterDir, 'cache'), { recursive: true })
|
|
28
|
+
await mkdir(join(meterDir, 'reports'), { recursive: true })
|
|
29
|
+
|
|
30
|
+
// Resolve true claude binary
|
|
31
|
+
const trueBinary = opts.trueBinary ?? await resolveTrueBinary('claude', binDir)
|
|
32
|
+
if (!trueBinary) {
|
|
33
|
+
throw new Error('claude not found in PATH. Install Claude Code first.')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Detect mode
|
|
37
|
+
const mode = opts.mode ?? await detectMode({ credentialsPath: CLAUDE_CREDENTIALS_PATH })
|
|
38
|
+
if (!mode) {
|
|
39
|
+
throw new Error('No Claude credentials found. Run `claude` first to authenticate.')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve org_id for Plan Mode
|
|
43
|
+
let orgId = opts.orgId ?? null
|
|
44
|
+
if (mode === 'plan' && orgId === null) {
|
|
45
|
+
const creds = await readCredentials(CLAUDE_CREDENTIALS_PATH)
|
|
46
|
+
if (creds) orgId = await resolveOrgId(creds)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Write shim
|
|
50
|
+
const shimPath = join(binDir, 'claude')
|
|
51
|
+
await writeShim(shimPath, trueBinary)
|
|
52
|
+
|
|
53
|
+
// Inject PATH (skip in tests)
|
|
54
|
+
if (!opts.skipPathInjection) {
|
|
55
|
+
const shell = detectShell()
|
|
56
|
+
const configPath = getShellConfigPath(shell)
|
|
57
|
+
const already = await isPathAlreadyInjected(configPath)
|
|
58
|
+
await injectPath(shell, configPath)
|
|
59
|
+
if (!already) {
|
|
60
|
+
console.log(`✓ Added ~/.meter/bin to PATH in ${configPath}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Write config
|
|
65
|
+
const config = ensureConfigDefaults({
|
|
66
|
+
mode,
|
|
67
|
+
resolved_binaries: { claude: trueBinary },
|
|
68
|
+
...(orgId ? { org_id: orgId } : {}),
|
|
69
|
+
})
|
|
70
|
+
await writeConfig(join(meterDir, 'config.json'), config)
|
|
71
|
+
|
|
72
|
+
// Install Claude Code hooks (skip in tests)
|
|
73
|
+
if (!opts.skipPathInjection) {
|
|
74
|
+
await installClaudeHooks(meterDir)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`✓ meter initialised (${mode} mode)`)
|
|
78
|
+
console.log(` Restart your terminal or run: source ~/.zshrc`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function installClaudeHooks(meterDir: string): Promise<void> {
|
|
82
|
+
const hooksDir = join(meterDir, 'hooks')
|
|
83
|
+
await mkdir(hooksDir, { recursive: true })
|
|
84
|
+
|
|
85
|
+
// Copy hook scripts to ~/.meter/hooks/
|
|
86
|
+
const srcDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'hooks')
|
|
87
|
+
for (const file of ['on-prompt.js', 'statusline.js']) {
|
|
88
|
+
try {
|
|
89
|
+
await copyFile(join(srcDir, file), join(hooksDir, file))
|
|
90
|
+
await chmod(join(hooksDir, file), 0o755)
|
|
91
|
+
} catch {
|
|
92
|
+
// If source doesn't exist (running from dist), try the dist/hooks path
|
|
93
|
+
const distDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'src', 'hooks')
|
|
94
|
+
try {
|
|
95
|
+
await copyFile(join(distDir, file), join(hooksDir, file))
|
|
96
|
+
await chmod(join(hooksDir, file), 0o755)
|
|
97
|
+
} catch {
|
|
98
|
+
console.warn(` ⚠ Could not copy ${file} — skipping hook installation`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Update Claude Code settings.json to add meter hooks
|
|
104
|
+
try {
|
|
105
|
+
let settings: Record<string, any> = {}
|
|
106
|
+
try {
|
|
107
|
+
settings = JSON.parse(await readFile(CLAUDE_SETTINGS_PATH, 'utf-8'))
|
|
108
|
+
} catch {
|
|
109
|
+
// settings file doesn't exist yet — we'll create it
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add UserPromptSubmit hook if not already present
|
|
113
|
+
const meterHookCommand = `node "${join(hooksDir, 'on-prompt.js')}"`
|
|
114
|
+
if (!settings.hooks) settings.hooks = {}
|
|
115
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = []
|
|
116
|
+
|
|
117
|
+
const alreadyHasHook = settings.hooks.UserPromptSubmit.some((h: any) =>
|
|
118
|
+
h.hooks?.some((hh: any) => hh.command?.includes('meter'))
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if (!alreadyHasHook) {
|
|
122
|
+
settings.hooks.UserPromptSubmit.push({
|
|
123
|
+
hooks: [{ type: 'command', command: meterHookCommand }]
|
|
124
|
+
})
|
|
125
|
+
console.log('✓ Added meter estimation hook to Claude Code')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Add/update statusline command
|
|
129
|
+
const meterStatuslineCommand = `node "${join(hooksDir, 'statusline.js')}"`
|
|
130
|
+
const existingStatusLine = settings.statusLine
|
|
131
|
+
|
|
132
|
+
if (!existingStatusLine || existingStatusLine.command?.includes('meter')) {
|
|
133
|
+
// No existing statusline or it's ours — set it
|
|
134
|
+
settings.statusLine = { type: 'command', command: meterStatuslineCommand }
|
|
135
|
+
console.log('✓ Added meter statusline to Claude Code')
|
|
136
|
+
} else {
|
|
137
|
+
// There's an existing statusline from another tool — chain them
|
|
138
|
+
// Create a wrapper that runs both
|
|
139
|
+
const chainCommand = `${existingStatusLine.command} && echo " │ " && ${meterStatuslineCommand}`
|
|
140
|
+
settings.statusLine = { type: 'command', command: chainCommand }
|
|
141
|
+
console.log('✓ Chained meter statusline with existing statusline')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await mkdir(dirname(CLAUDE_SETTINGS_PATH), { recursive: true })
|
|
145
|
+
await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8')
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn(` ⚠ Could not update Claude Code settings: ${err}`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readConfig } from '../storage/config-store.js'
|
|
2
|
+
import { openDb, getRecentTasks } from '../storage/db.js'
|
|
3
|
+
import { CONFIG_PATH, HISTORY_DB_PATH } from '../constants.js'
|
|
4
|
+
|
|
5
|
+
export async function runReport(): Promise<void> {
|
|
6
|
+
const config = await readConfig(CONFIG_PATH)
|
|
7
|
+
if (!config) { console.log('Run meter init first.'); return }
|
|
8
|
+
const db = await openDb(HISTORY_DB_PATH)
|
|
9
|
+
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
|
|
10
|
+
const allTasks = getRecentTasks(db, 500).filter(t => t.created_at >= weekAgo)
|
|
11
|
+
db.close()
|
|
12
|
+
if (allTasks.length === 0) { console.log('No tasks recorded this week.'); return }
|
|
13
|
+
const totalCost = allTasks.reduce((s, t) => s + (t.actual_cost ?? 0), 0)
|
|
14
|
+
const heaviest = allTasks.sort((a, b) => (b.actual_cost ?? 0) - (a.actual_cost ?? 0))[0]
|
|
15
|
+
const byComplexity: Record<string, number> = { low: 0, medium: 0, heavy: 0, critical: 0 }
|
|
16
|
+
for (const t of allTasks) byComplexity[t.complexity] = (byComplexity[t.complexity] ?? 0) + 1
|
|
17
|
+
console.log(`\n◆ meter weekly report\n`)
|
|
18
|
+
console.log(` Mode ${config.mode}`)
|
|
19
|
+
console.log(` Tasks run ${allTasks.length}`)
|
|
20
|
+
if (config.mode === 'api') {
|
|
21
|
+
console.log(` Total spend $${totalCost.toFixed(3)}`)
|
|
22
|
+
console.log(` Avg task cost $${(totalCost / allTasks.length).toFixed(3)}`)
|
|
23
|
+
}
|
|
24
|
+
if (heaviest) console.log(` Heaviest task "${heaviest.prompt_text.slice(0, 40)}"`)
|
|
25
|
+
console.log(`\n By complexity:`)
|
|
26
|
+
for (const [k, v] of Object.entries(byComplexity)) if (v > 0) console.log(` ${k.padEnd(10)} ${v} tasks`)
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readConfig } from '../storage/config-store.js'
|
|
2
|
+
import { CONFIG_PATH } from '../constants.js'
|
|
3
|
+
|
|
4
|
+
export async function runStatus(): Promise<void> {
|
|
5
|
+
const config = await readConfig(CONFIG_PATH)
|
|
6
|
+
if (!config) { console.log('meter not initialised. Run: meter init'); return }
|
|
7
|
+
console.log(`◆ meter status\n`)
|
|
8
|
+
console.log(` Mode: ${config.mode}`)
|
|
9
|
+
console.log(` Model: ${config.models.claude_chain[0]}`)
|
|
10
|
+
console.log(` Claude: ${config.resolved_binaries.claude}`)
|
|
11
|
+
if (config.mode === 'api') {
|
|
12
|
+
console.log(` Budget: $${config.budget.per_task_usd} per task (notify at ${config.budget.threshold_pct}%)`)
|
|
13
|
+
} else {
|
|
14
|
+
console.log(` Window: notify at ${config.plan.window_threshold_pct}%`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { rm } from 'fs/promises'
|
|
2
|
+
import { removePath } from '../shell/path-inject.js'
|
|
3
|
+
import { detectShell, getShellConfigPath } from '../shell/detect.js'
|
|
4
|
+
import { METER_DIR } from '../constants.js'
|
|
5
|
+
import * as readline from 'readline/promises'
|
|
6
|
+
|
|
7
|
+
export async function runUninstall(): Promise<void> {
|
|
8
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
9
|
+
const answer = await rl.question('Remove all meter data (~/.meter/)? [y/N] ')
|
|
10
|
+
rl.close()
|
|
11
|
+
const shell = detectShell()
|
|
12
|
+
const configPath = getShellConfigPath(shell)
|
|
13
|
+
await removePath(configPath)
|
|
14
|
+
console.log(`✓ Removed PATH entry from ${configPath}`)
|
|
15
|
+
if (answer.toLowerCase() === 'y') {
|
|
16
|
+
await rm(METER_DIR, { recursive: true, force: true })
|
|
17
|
+
console.log('✓ Removed ~/.meter/')
|
|
18
|
+
}
|
|
19
|
+
console.log('✓ Uninstall complete. Run: npm uninstall -g meter-ai')
|
|
20
|
+
}
|