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.
- 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
|
@@ -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
|
+
}
|