meter-ai 0.2.0 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAWA,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,CAmD9D"}
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"}
@@ -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":"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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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meter-ai",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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"
@@ -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
+ }