modelmeter-collect 0.9.0 → 0.10.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.
Files changed (4) hide show
  1. package/README.md +12 -2
  2. package/cli.mjs +50 -6
  3. package/lib.mjs +42 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -12,13 +12,23 @@ It dedupes (state in `~/.modelmeter/collector-state.json`), so every run is safe
12
12
  Not covered: the **ChatGPT** consumer app (no per-message token data exists) and **Cursor**
13
13
  on a Pro plan (usage stays on Cursor's servers).
14
14
 
15
- ## Quick start
15
+ ## Try it, no account
16
+
17
+ ```bash
18
+ # See where your Claude Code / Codex tokens go - signals + tips, runs locally:
19
+ npx modelmeter-collect
20
+ ```
21
+
22
+ With no token configured this reads your local logs and prints your cache reuse, top MCP
23
+ server, context-bloat, and recommendations. Nothing is sent.
24
+
25
+ ## Track it over time
16
26
 
17
27
  ```bash
18
28
  # 1. Grab an ingest token from the Providers tab at https://modelmeter.dev, then:
19
29
  npx modelmeter-collect init mm_live_xxxxxxxx
20
30
 
21
- # 2. Backfill the last couple of weeks:
31
+ # 2. Backfill the last couple of weeks (now that a token is saved, it reports usage):
22
32
  npx modelmeter-collect
23
33
 
24
34
  # Preview what would be sent, without sending:
package/cli.mjs CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  claudeSessionSummary,
24
24
  codexSessionSummary,
25
25
  buildLocalRecommendations,
26
+ summarizeSignals,
26
27
  } from './lib.mjs'
27
28
 
28
29
  const HOME = homedir()
@@ -72,8 +73,9 @@ Commands:
72
73
  status, and exactly what would be sent. Add --recommendations for
73
74
  local optimization tips (cache, MCP, output, context bloat), or
74
75
  --payload for the raw JSON (token counts only, never transcript text).
75
- (none) Scan Claude Code + Codex logs and report token counts. Deduped,
76
- so it is safe to run repeatedly. MODELMETER_DRYRUN=1 previews only.
76
+ (none) No token yet? Shows your local optimization signals + tips from
77
+ Claude Code / Codex logs, no account needed. With a token saved,
78
+ it scans and reports usage (deduped, safe to run repeatedly).
77
79
 
78
80
  It sends only model names and token counts. Never your prompts, never your keys.
79
81
  Get an ingest token from the Providers tab at https://modelmeter.dev`)
@@ -185,7 +187,7 @@ function recentSessionFiles(dir, cutoffMs, limit) {
185
187
  .map((x) => x.p)
186
188
  }
187
189
 
188
- function printRecommendations(cutoffMs) {
190
+ function readSummaries(cutoffMs) {
189
191
  const summaries = []
190
192
  const sources = [
191
193
  [join(HOME, '.claude', 'projects'), claudeSessionSummary],
@@ -203,6 +205,27 @@ function printRecommendations(cutoffMs) {
203
205
  if (summary) summaries.push(summary)
204
206
  }
205
207
  }
208
+ return summaries
209
+ }
210
+
211
+ function pct(x) {
212
+ return `${Math.round(x * 100)}%`
213
+ }
214
+
215
+ // A glance at the optimization signals (the same dimensions ModelMeter surfaces), so
216
+ // the command shows value, not just discovery.
217
+ function printSignals(summaries) {
218
+ if (summaries.length === 0) return
219
+ const s = summarizeSignals(summaries)
220
+ console.log(`\nOptimization signals (last 14 days, ${s.sessions} sessions, computed locally):`)
221
+ console.log(` cache reuse: ${pct(s.cacheRate)}`)
222
+ console.log(` output share: ${pct(s.outputShare)}`)
223
+ if (s.reasoningShare >= 0.005) console.log(` reasoning: ${pct(s.reasoningShare)} of output`)
224
+ if (s.topMcp) console.log(` top MCP: ${s.topMcp} (${pct(s.topMcpShare)} of tool tokens)`)
225
+ console.log(` context bloat: ${s.bloatedSessions} session${s.bloatedSessions === 1 ? '' : 's'} grew past 2x`)
226
+ }
227
+
228
+ function printRecommendations(summaries) {
206
229
  console.log('\nLocal recommendations (computed from your logs, nothing sent):')
207
230
  const recs = buildLocalRecommendations(summaries)
208
231
  if (recs.length === 0) {
@@ -233,7 +256,7 @@ if (cmd === 'doctor') {
233
256
  }),
234
257
  )
235
258
  if (args.includes('--recommendations')) {
236
- printRecommendations(cutoffMs)
259
+ printRecommendations(readSummaries(cutoffMs))
237
260
  process.exit(0)
238
261
  }
239
262
  if (args.includes('--payload')) {
@@ -241,6 +264,7 @@ if (cmd === 'doctor') {
241
264
  process.env.MODELMETER_DRYRUN = '1'
242
265
  await runCollector() // prints the exact payload (counts only), then exits
243
266
  } else {
267
+ printSignals(readSummaries(cutoffMs))
244
268
  console.log(
245
269
  '\nRun `npx modelmeter-collect doctor --recommendations` for local optimization tips, or',
246
270
  )
@@ -249,5 +273,25 @@ if (cmd === 'doctor') {
249
273
  }
250
274
  }
251
275
 
252
- // Default: scan and report.
253
- await runCollector()
276
+ // Default: with a token configured, scan and report (the hook / scheduled path).
277
+ // Without one, this is a new user trying it - show the local value, no account needed.
278
+ const cfg = readConfig()
279
+ const configured = Boolean(process.env.MODELMETER_TOKEN || cfg.token)
280
+ if (configured) {
281
+ await runCollector()
282
+ } else {
283
+ const cutoffMs = Date.now() - 14 * 86_400_000
284
+ const summaries = readSummaries(cutoffMs)
285
+ console.log('ModelMeter: where your Claude Code and Codex tokens go. Local, token counts only.\n')
286
+ if (summaries.length === 0) {
287
+ console.log('No Claude Code or Codex session logs found in the last 14 days.')
288
+ console.log('Use one of those tools, then run `npx modelmeter-collect` again.')
289
+ process.exit(0)
290
+ }
291
+ printSignals(summaries)
292
+ printRecommendations(summaries)
293
+ console.log('\nThis ran entirely on your machine. Nothing was sent.')
294
+ console.log('Track it over time, across repos, or for your team: get a token at')
295
+ console.log('https://modelmeter.dev, then `npx modelmeter-collect init <token>`.')
296
+ process.exit(0)
297
+ }
package/lib.mjs CHANGED
@@ -589,3 +589,45 @@ export function buildLocalRecommendations(summaries) {
589
589
 
590
590
  return recs
591
591
  }
592
+
593
+ // Headline optimization signals (metrics, not recommendations) for `doctor` to show
594
+ // at a glance: the same dimensions ModelMeter surfaces, computed locally.
595
+ export function summarizeSignals(summaries) {
596
+ const list = (summaries || []).filter(Boolean)
597
+ let uncached = 0
598
+ let cacheRead = 0
599
+ let cacheCreate = 0
600
+ let output = 0
601
+ let reasoning = 0
602
+ let bloated = 0
603
+ const tools = {}
604
+ for (const s of list) {
605
+ uncached += s.uncached
606
+ cacheRead += s.cacheRead
607
+ cacheCreate += s.cacheCreate
608
+ output += s.output
609
+ reasoning += s.reasoning || 0
610
+ const b = sessionBloat(s.contextSeq)
611
+ if (b && b.last5 > b.first5 * 2 && b.last5 > 30_000) bloated++
612
+ for (const [g, v] of Object.entries(s.tools || {})) {
613
+ tools[g] = (tools[g] || 0) + v.tokens
614
+ }
615
+ }
616
+ const inputTotal = uncached + cacheRead + cacheCreate
617
+ const total = inputTotal + output
618
+ const toolArr = Object.entries(tools)
619
+ .map(([tool, tok]) => ({ tool, tok }))
620
+ .sort((a, b) => b.tok - a.tok)
621
+ const toolTotal = toolArr.reduce((n, t) => n + t.tok, 0)
622
+ const topMcp = toolArr.find((t) => t.tool.startsWith('mcp:'))
623
+ return {
624
+ sessions: list.length,
625
+ totalTokens: total,
626
+ cacheRate: inputTotal > 0 ? cacheRead / inputTotal : 0,
627
+ outputShare: total > 0 ? output / total : 0,
628
+ reasoningShare: output > 0 ? reasoning / output : 0,
629
+ topMcp: topMcp ? topMcp.tool : null,
630
+ topMcpShare: topMcp && toolTotal > 0 ? topMcp.tok / toolTotal : 0,
631
+ bloatedSessions: bloated,
632
+ }
633
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelmeter-collect",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Report LLM token usage from local Claude Code / Codex logs to ModelMeter. Token counts only, never prompts or keys.",
5
5
  "type": "module",
6
6
  "bin": {