moflo 4.9.34 → 4.9.35

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.
@@ -5,6 +5,12 @@
5
5
  * the JSON shape consumed by The Luminarium's "Claude Stats" tab. Pure I/O
6
6
  * + reduce; no persistent storage, no network.
7
7
  *
8
+ * Scope: primary-session transcripts only (top-level `*.jsonl`). Per-session
9
+ * `<id>/subagents/agent-*.jsonl` files (Task-tool spawns) are NOT walked, so
10
+ * Sonnet/Haiku usage from /simplify, /ultrareview, etc. is missing from the
11
+ * model distribution. Tracked as a follow-up — when wiring it up, recurse
12
+ * into `subagents/` and roll the subagent mtimes/sizes into the cache key.
13
+ *
8
14
  * Performance posture:
9
15
  * - Streaming readline (not `readFileSync().split('\n')`) — transcripts
10
16
  * can grow to tens of MB and we don't want to balloon dashboard memory.
@@ -19,7 +25,24 @@ import { stat, readdir } from 'node:fs/promises';
19
25
  import { homedir } from 'node:os';
20
26
  import { join } from 'node:path';
21
27
  import { createInterface } from 'node:readline';
22
- import { canonicalModelKey, costFromUsage, rateForModel } from './claude-model-rates.js';
28
+ /**
29
+ * Map a transcript model name to a stable display key. Recognises bare family
30
+ * names ("opus", "sonnet", "haiku"), dated/dotted variants
31
+ * ("claude-opus-4-7", "claude-3-5-sonnet-20241022"), and is case-insensitive.
32
+ * Anything else falls through to `'unknown'`.
33
+ */
34
+ export function canonicalModelKey(model) {
35
+ if (!model || typeof model !== 'string')
36
+ return 'unknown';
37
+ const lower = model.toLowerCase();
38
+ if (lower.includes('opus'))
39
+ return 'opus';
40
+ if (lower.includes('sonnet'))
41
+ return 'sonnet';
42
+ if (lower.includes('haiku'))
43
+ return 'haiku';
44
+ return 'unknown';
45
+ }
23
46
  // ============================================================================
24
47
  // Path resolution
25
48
  // ============================================================================
@@ -46,7 +69,7 @@ export function _resetClaudeStatsCache() {
46
69
  // ============================================================================
47
70
  const DAY_MS = 86_400_000;
48
71
  function emptyWindow() {
49
- return { sessions: new Set(), input: 0, output: 0, cacheCreate: 0, cacheRead: 0, costUsd: 0 };
72
+ return { sessions: new Set(), input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
50
73
  }
51
74
  function freezeWindow(w) {
52
75
  const total = w.input + w.output + w.cacheCreate + w.cacheRead;
@@ -59,12 +82,8 @@ function freezeWindow(w) {
59
82
  cacheRead: w.cacheRead,
60
83
  total,
61
84
  },
62
- costUsd: round4(w.costUsd),
63
85
  };
64
86
  }
65
- function round4(n) {
66
- return Math.round(n * 10_000) / 10_000;
67
- }
68
87
  function makeAggregator() {
69
88
  return {
70
89
  today: emptyWindow(),
@@ -126,31 +145,28 @@ function consumeLine(agg, line, now) {
126
145
  const cr = usage.cache_read_input_tokens ?? 0;
127
146
  const totalThisLine = input + output + cc + cr;
128
147
  const modelKey = canonicalModelKey(line.message?.model);
129
- const rate = rateForModel(line.message?.model);
130
- const cost = costFromUsage(rate, { input, output, cacheCreate: cc, cacheRead: cr });
131
148
  agg.modelMessages.set(modelKey, (agg.modelMessages.get(modelKey) ?? 0) + 1);
132
149
  agg.modelTokens.set(modelKey, (agg.modelTokens.get(modelKey) ?? 0) + totalThisLine);
133
150
  // Lifetime always.
134
- bump(agg.lifetime, sessionId, input, output, cc, cr, cost);
151
+ bump(agg.lifetime, sessionId, input, output, cc, cr);
135
152
  // Bucketed windows — only when we have a usable timestamp.
136
153
  if (Number.isFinite(ts)) {
137
154
  const ageMs = now - ts;
138
155
  if (ageMs >= 0 && ageMs < DAY_MS)
139
- bump(agg.today, sessionId, input, output, cc, cr, cost);
156
+ bump(agg.today, sessionId, input, output, cc, cr);
140
157
  if (ageMs >= 0 && ageMs < 7 * DAY_MS)
141
- bump(agg.last7d, sessionId, input, output, cc, cr, cost);
158
+ bump(agg.last7d, sessionId, input, output, cc, cr);
142
159
  if (ageMs >= 0 && ageMs < 30 * DAY_MS)
143
- bump(agg.last30d, sessionId, input, output, cc, cr, cost);
160
+ bump(agg.last30d, sessionId, input, output, cc, cr);
144
161
  }
145
162
  }
146
- function bump(w, sessionId, input, output, cc, cr, cost) {
163
+ function bump(w, sessionId, input, output, cc, cr) {
147
164
  if (sessionId)
148
165
  w.sessions.add(sessionId);
149
166
  w.input += input;
150
167
  w.output += output;
151
168
  w.cacheCreate += cc;
152
169
  w.cacheRead += cr;
153
- w.costUsd += cost;
154
170
  }
155
171
  // ============================================================================
156
172
  // File walking
@@ -1056,25 +1056,19 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
1056
1056
  if (n < 1_000_000_000) return (n / 1_000_000).toFixed(2) + 'M';
1057
1057
  return (n / 1_000_000_000).toFixed(2) + 'B';
1058
1058
  };
1059
- // USD with two decimals; sub-cent renders as "<$0.01".
1060
- const fmtUsd = (n) => {
1061
- if (n == null) return '-';
1062
- if (n === 0) return '$0.00';
1063
- if (n < 0.01) return '<$0.01';
1064
- return '$' + n.toFixed(2);
1065
- };
1066
-
1067
1059
  function renderClaudeStats(cs) {
1068
1060
  const el = document.getElementById('panel-claude-stats');
1069
1061
  if (!cs) { el.innerHTML = '<div class="empty">Loading...</div>'; return; }
1070
1062
 
1071
- // Always-visible disclaimer banner — kept verbatim per the issue's
1072
- // wording so the user understands the scope and limits at a glance.
1063
+ // Always-visible disclaimer banner — keeps the scope and limits in
1064
+ // view so the numbers aren't read as account-wide truth.
1073
1065
  const disclaimer =
1074
1066
  '<div style="background:#161b22;border:1px solid #30363d;border-left:3px solid #d29922;border-radius:6px;padding:10px 14px;margin-bottom:16px;color:#c9d1d9;font-size:0.85rem;line-height:1.5">' +
1075
- '<strong>Local stats only.</strong> Counts what Claude Code wrote to disk for THIS project on THIS machine. ' +
1076
- 'Doesn\\'t include claude.ai web sessions, other projects, other devices, or your account-level plan quota. ' +
1077
- 'Cost is a local estimate using current public model rates.' +
1067
+ '<strong>Local primary-session stats only.</strong> Counts what Claude Code wrote to disk for THIS project on THIS machine. ' +
1068
+ '<strong>Excludes sub-sessions spawned by Task-tool agents</strong> ' +
1069
+ '(e.g. <code>/simplify</code>, <code>/ultrareview</code>, Explore) their Sonnet/Haiku usage is stored in per-session ' +
1070
+ '<code>subagents/</code> transcripts the dashboard doesn\\'t yet read, so totals here skew toward the primary model. ' +
1071
+ 'Also excludes claude.ai web sessions, other projects, other devices, and your account-level plan quota.' +
1078
1072
  '</div>';
1079
1073
 
1080
1074
  if (!cs.available) {
@@ -1095,14 +1089,13 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
1095
1089
  '<td>' + fmtCount(win.tokens.input) + '</td>' +
1096
1090
  '<td>' + fmtCount(win.tokens.output) + '</td>' +
1097
1091
  '<td>' + fmtCount(win.tokens.cacheCreate) + '</td>' +
1098
- '<td>' + fmtCount(win.tokens.cacheRead) + '</td>' +
1099
- '<td>' + fmtUsd(win.costUsd) + '</td></tr>';
1092
+ '<td>' + fmtCount(win.tokens.cacheRead) + '</td></tr>';
1100
1093
  };
1101
1094
  const winTable =
1102
- '<h2>Sessions, Tokens, Cost</h2>' +
1095
+ '<h2>Sessions and Tokens</h2>' +
1103
1096
  '<table><thead><tr>' +
1104
1097
  '<th>Window</th><th>Sessions</th><th>Total Tokens</th>' +
1105
- '<th>Input</th><th>Output</th><th>Cache Create</th><th>Cache Read</th><th>Est. Cost</th>' +
1098
+ '<th>Input</th><th>Output</th><th>Cache Create</th><th>Cache Read</th>' +
1106
1099
  '</tr></thead><tbody>' +
1107
1100
  winRow('Today', w.today) +
1108
1101
  winRow('Last 7 days', w.last7d) +
@@ -1117,7 +1110,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
1117
1110
  '<td>' + fmtCount(m.tokens) + '</td></tr>').join('')
1118
1111
  : '<tr><td colspan="3" class="empty">No model data</td></tr>';
1119
1112
  const modelsTable =
1120
- '<h2>Models Used</h2>' +
1113
+ '<h2>Models Used (primary sessions)</h2>' +
1121
1114
  '<table><thead><tr><th>Model</th><th>Messages</th><th>Total Tokens</th></tr></thead>' +
1122
1115
  '<tbody>' + modelRows + '</tbody></table>';
1123
1116
 
@@ -1139,11 +1132,9 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
1139
1132
  '<div class="stat-card"><div class="label">p95 Duration</div><div class="value">' + fmtDuration(cs.sessionDurationMs.p95) + '</div></div>' +
1140
1133
  '</div>';
1141
1134
 
1142
- // Footer note linking to the rate-table source comment.
1143
1135
  const footer =
1144
1136
  '<div class="dim" style="margin-top:12px">' +
1145
- 'Cost estimate uses rates from <code>src/cli/services/claude-model-rates.ts</code> ' +
1146
- '(USD/1M tokens, list price). Aggregation took ' + fmtDuration(cs.elapsedMs) +
1137
+ 'Aggregation took ' + fmtDuration(cs.elapsedMs) +
1147
1138
  (cs.parseErrors ? ' &middot; ' + cs.parseErrors + ' lines skipped (parse error)' : '') +
1148
1139
  '</div>';
1149
1140
 
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.9.34';
5
+ export const VERSION = '4.9.35';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.34",
3
+ "version": "4.9.35",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -97,7 +97,7 @@
97
97
  "@typescript-eslint/eslint-plugin": "^7.18.0",
98
98
  "@typescript-eslint/parser": "^7.18.0",
99
99
  "eslint": "^8.0.0",
100
- "moflo": "^4.9.33",
100
+ "moflo": "^4.9.34",
101
101
  "tsx": "^4.21.0",
102
102
  "typescript": "^5.9.3",
103
103
  "vitest": "^4.0.0"
@@ -1,54 +0,0 @@
1
- /**
2
- * Per-model USD/1M rates. Lookup goes through {@link rateForModel}, which
3
- * normalises the transcript's loose model names ("opus", "claude-opus-4-7",
4
- * "haiku-4-5", etc.) onto a canonical key.
5
- */
6
- export const CLAUDE_RATES = {
7
- // Opus 4.x family
8
- opus: { input: 15, output: 75, cacheCreate: 18.75, cacheRead: 1.5 },
9
- // Sonnet 4.x family
10
- sonnet: { input: 3, output: 15, cacheCreate: 3.75, cacheRead: 0.3 },
11
- // Haiku 4.x family
12
- haiku: { input: 0.8, output: 4, cacheCreate: 1, cacheRead: 0.08 },
13
- // Unknown — zeroed out so the cost UI shows $0 for unrecognised models
14
- // rather than guessing wrong. The model name still surfaces in the
15
- // distribution card so the user can see the gap.
16
- unknown: { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
17
- };
18
- /** Canonical model keys, ordered most-expensive → cheapest for stable display. */
19
- export const CANONICAL_MODELS = ['opus', 'sonnet', 'haiku', 'unknown'];
20
- /**
21
- * Map a transcript model name to a canonical rate key. Recognises:
22
- * - bare family names: "opus", "sonnet", "haiku"
23
- * - dated/dotted variants: "claude-opus-4-7", "claude-3-5-sonnet-20241022"
24
- * - case-insensitive
25
- * Anything else falls through to `'unknown'`.
26
- */
27
- export function canonicalModelKey(model) {
28
- if (!model || typeof model !== 'string')
29
- return 'unknown';
30
- const lower = model.toLowerCase();
31
- if (lower.includes('opus'))
32
- return 'opus';
33
- if (lower.includes('sonnet'))
34
- return 'sonnet';
35
- if (lower.includes('haiku'))
36
- return 'haiku';
37
- return 'unknown';
38
- }
39
- /** Resolve a `ClaudeRate` row for the given (possibly raw) model name. */
40
- export function rateForModel(model) {
41
- return CLAUDE_RATES[canonicalModelKey(model)] ?? CLAUDE_RATES.unknown;
42
- }
43
- /**
44
- * Compute USD cost for a single usage record.
45
- * Rates are per 1M tokens; divide by 1e6 once at the end.
46
- */
47
- export function costFromUsage(rate, usage) {
48
- const i = usage.input ?? 0;
49
- const o = usage.output ?? 0;
50
- const cc = usage.cacheCreate ?? 0;
51
- const cr = usage.cacheRead ?? 0;
52
- return ((i * rate.input + o * rate.output + cc * rate.cacheCreate + cr * rate.cacheRead) / 1_000_000);
53
- }
54
- //# sourceMappingURL=claude-model-rates.js.map