moflo 4.9.34 → 4.9.36
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/src/cli/init/moflo-init.js +13 -5
- package/dist/src/cli/memory/auto-memory-bridge.js +8 -11
- package/dist/src/cli/services/claude-stats.js +29 -27
- package/dist/src/cli/services/daemon-dashboard.js +12 -21
- package/dist/src/cli/shared/utils/claude-projects-path.js +32 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/services/claude-model-rates.js +0 -54
|
@@ -435,7 +435,7 @@ function generateClaudeMd(root, _force) {
|
|
|
435
435
|
// scriptFiles array in bin/session-start-launcher.mjs — first-init drops any
|
|
436
436
|
// script missing here, and the launcher's manifest cleanup later treats it as
|
|
437
437
|
// orphan residue and deletes it (#777, feedback_scriptfiles_sync.md).
|
|
438
|
-
const SCRIPT_MAP = [
|
|
438
|
+
export const SCRIPT_MAP = [
|
|
439
439
|
'hooks.mjs',
|
|
440
440
|
'session-start-launcher.mjs',
|
|
441
441
|
'index-guidance.mjs',
|
|
@@ -496,17 +496,25 @@ function isStale(srcPath, destPath) {
|
|
|
496
496
|
// ============================================================================
|
|
497
497
|
// Step 6: .gitignore
|
|
498
498
|
// ============================================================================
|
|
499
|
-
function updateGitignore(root) {
|
|
499
|
+
export function updateGitignore(root) {
|
|
500
500
|
const gitignorePath = path.join(root, '.gitignore');
|
|
501
501
|
const entries = [
|
|
502
502
|
'.claude-epic/',
|
|
503
503
|
'.moflo/',
|
|
504
504
|
'.swarm/',
|
|
505
|
-
'.moflo/',
|
|
506
505
|
'.claude/settings.local.json',
|
|
507
506
|
'.claude/scheduled_tasks.lock',
|
|
508
507
|
'**/workflow-state.json',
|
|
508
|
+
// Leading `/` anchors to gitignore root — bare `.claude/guidance/` once
|
|
509
|
+
// swallowed shipped/internal subdirs and broke `npm pack`
|
|
510
|
+
// (guidance-gitignore-shipped-trap).
|
|
511
|
+
'/.claude/guidance/moflo-*.md',
|
|
512
|
+
...SCRIPT_MAP.map(name => `/.claude/scripts/${name}`),
|
|
509
513
|
];
|
|
514
|
+
// Treat `/.foo` and `.foo` as the same rule when checking for prior presence
|
|
515
|
+
// — both forms anchor at gitignore root, so a consumer who wrote either
|
|
516
|
+
// shouldn't get a duplicate appended.
|
|
517
|
+
const normalize = (s) => s.replace(/^\//, '');
|
|
510
518
|
if (!fs.existsSync(gitignorePath)) {
|
|
511
519
|
const defaultEntries = ['node_modules/', 'dist/', '.env', '.env.*', ''];
|
|
512
520
|
const content = '# Dependencies\n' + defaultEntries.join('\n') + '\n# MoFlo state\n' + entries.join('\n') + '\n';
|
|
@@ -514,8 +522,8 @@ function updateGitignore(root) {
|
|
|
514
522
|
return { name: '.gitignore', status: 'created', detail: 'Created with node_modules, .env, and MoFlo entries' };
|
|
515
523
|
}
|
|
516
524
|
const existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
517
|
-
const existingLines = new Set(existing.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#')));
|
|
518
|
-
const toAdd = entries.filter(e => !existingLines.has(e));
|
|
525
|
+
const existingLines = new Set(existing.split(/\r?\n/).map(l => normalize(l.trim())).filter(l => l && !l.startsWith('#')));
|
|
526
|
+
const toAdd = entries.filter(e => !existingLines.has(normalize(e)));
|
|
519
527
|
if (toAdd.length === 0) {
|
|
520
528
|
return { name: '.gitignore', status: 'skipped', detail: 'Entries already present' };
|
|
521
529
|
}
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* @module moflo/cli/memory/auto-memory-bridge
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
15
|
import { EventEmitter } from 'node:events';
|
|
17
16
|
import * as fs from 'node:fs/promises';
|
|
18
17
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
19
18
|
import * as path from 'node:path';
|
|
19
|
+
import { claudeProjectDirFor } from '../shared/utils/claude-projects-path.js';
|
|
20
20
|
import { createDefaultEntry, } from './types.js';
|
|
21
21
|
import { LearningBridge } from './learning-bridge.js';
|
|
22
22
|
import { MemoryGraph } from './memory-graph.js';
|
|
@@ -539,20 +539,17 @@ export class AutoMemoryBridge extends EventEmitter {
|
|
|
539
539
|
// ===== Utility Functions =====
|
|
540
540
|
/**
|
|
541
541
|
* Resolve the auto memory directory for a given working directory.
|
|
542
|
-
*
|
|
542
|
+
*
|
|
543
|
+
* The git root is preferred over the raw cwd so a session in
|
|
544
|
+
* `<repo>/packages/foo` resolves to the repo's auto-memory store, matching
|
|
545
|
+
* Claude Code's own behaviour. The encoded suffix is produced by the shared
|
|
546
|
+
* `claudeProjectDirFor` helper so this bridge cannot drift away from the
|
|
547
|
+
* Claude Code directory naming convention (issue #1048).
|
|
543
548
|
*/
|
|
544
549
|
export function resolveAutoMemoryDir(workingDir) {
|
|
545
550
|
const gitRoot = findGitRoot(workingDir);
|
|
546
551
|
const basePath = gitRoot || workingDir;
|
|
547
|
-
|
|
548
|
-
// The leading dash IS preserved (e.g. /workspaces/foo -> -workspaces-foo)
|
|
549
|
-
// On Windows, strip drive letter prefix (C:) for cleaner keys
|
|
550
|
-
let normalized = basePath.split(path.sep).join('/');
|
|
551
|
-
if (process.platform === 'win32') {
|
|
552
|
-
normalized = normalized.replace(/^[A-Za-z]:/, '');
|
|
553
|
-
}
|
|
554
|
-
const projectKey = normalized.replace(/\//g, '-');
|
|
555
|
-
return path.join(homedir(), '.claude', 'projects', projectKey, 'memory');
|
|
552
|
+
return path.join(claudeProjectDirFor(basePath), 'memory');
|
|
556
553
|
}
|
|
557
554
|
/**
|
|
558
555
|
* Find the git root directory by walking up from workingDir.
|
|
@@ -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.
|
|
@@ -16,24 +22,27 @@
|
|
|
16
22
|
*/
|
|
17
23
|
import { createReadStream } from 'node:fs';
|
|
18
24
|
import { stat, readdir } from 'node:fs/promises';
|
|
19
|
-
import { homedir } from 'node:os';
|
|
20
25
|
import { join } from 'node:path';
|
|
21
26
|
import { createInterface } from 'node:readline';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
// Path resolution
|
|
25
|
-
// ============================================================================
|
|
27
|
+
import { claudeProjectDirFor, encodeCwdForClaudeProjects, } from '../shared/utils/claude-projects-path.js';
|
|
28
|
+
export { claudeProjectDirFor, encodeCwdForClaudeProjects };
|
|
26
29
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
+
* Map a transcript model name to a stable display key. Recognises bare family
|
|
31
|
+
* names ("opus", "sonnet", "haiku"), dated/dotted variants
|
|
32
|
+
* ("claude-opus-4-7", "claude-3-5-sonnet-20241022"), and is case-insensitive.
|
|
33
|
+
* Anything else falls through to `'unknown'`.
|
|
30
34
|
*/
|
|
31
|
-
export function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
export function canonicalModelKey(model) {
|
|
36
|
+
if (!model || typeof model !== 'string')
|
|
37
|
+
return 'unknown';
|
|
38
|
+
const lower = model.toLowerCase();
|
|
39
|
+
if (lower.includes('opus'))
|
|
40
|
+
return 'opus';
|
|
41
|
+
if (lower.includes('sonnet'))
|
|
42
|
+
return 'sonnet';
|
|
43
|
+
if (lower.includes('haiku'))
|
|
44
|
+
return 'haiku';
|
|
45
|
+
return 'unknown';
|
|
37
46
|
}
|
|
38
47
|
const CACHE_TTL_MS = 30_000;
|
|
39
48
|
let cache = null;
|
|
@@ -46,7 +55,7 @@ export function _resetClaudeStatsCache() {
|
|
|
46
55
|
// ============================================================================
|
|
47
56
|
const DAY_MS = 86_400_000;
|
|
48
57
|
function emptyWindow() {
|
|
49
|
-
return { sessions: new Set(), input: 0, output: 0, cacheCreate: 0, cacheRead: 0
|
|
58
|
+
return { sessions: new Set(), input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
|
|
50
59
|
}
|
|
51
60
|
function freezeWindow(w) {
|
|
52
61
|
const total = w.input + w.output + w.cacheCreate + w.cacheRead;
|
|
@@ -59,12 +68,8 @@ function freezeWindow(w) {
|
|
|
59
68
|
cacheRead: w.cacheRead,
|
|
60
69
|
total,
|
|
61
70
|
},
|
|
62
|
-
costUsd: round4(w.costUsd),
|
|
63
71
|
};
|
|
64
72
|
}
|
|
65
|
-
function round4(n) {
|
|
66
|
-
return Math.round(n * 10_000) / 10_000;
|
|
67
|
-
}
|
|
68
73
|
function makeAggregator() {
|
|
69
74
|
return {
|
|
70
75
|
today: emptyWindow(),
|
|
@@ -126,31 +131,28 @@ function consumeLine(agg, line, now) {
|
|
|
126
131
|
const cr = usage.cache_read_input_tokens ?? 0;
|
|
127
132
|
const totalThisLine = input + output + cc + cr;
|
|
128
133
|
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
134
|
agg.modelMessages.set(modelKey, (agg.modelMessages.get(modelKey) ?? 0) + 1);
|
|
132
135
|
agg.modelTokens.set(modelKey, (agg.modelTokens.get(modelKey) ?? 0) + totalThisLine);
|
|
133
136
|
// Lifetime always.
|
|
134
|
-
bump(agg.lifetime, sessionId, input, output, cc, cr
|
|
137
|
+
bump(agg.lifetime, sessionId, input, output, cc, cr);
|
|
135
138
|
// Bucketed windows — only when we have a usable timestamp.
|
|
136
139
|
if (Number.isFinite(ts)) {
|
|
137
140
|
const ageMs = now - ts;
|
|
138
141
|
if (ageMs >= 0 && ageMs < DAY_MS)
|
|
139
|
-
bump(agg.today, sessionId, input, output, cc, cr
|
|
142
|
+
bump(agg.today, sessionId, input, output, cc, cr);
|
|
140
143
|
if (ageMs >= 0 && ageMs < 7 * DAY_MS)
|
|
141
|
-
bump(agg.last7d, sessionId, input, output, cc, cr
|
|
144
|
+
bump(agg.last7d, sessionId, input, output, cc, cr);
|
|
142
145
|
if (ageMs >= 0 && ageMs < 30 * DAY_MS)
|
|
143
|
-
bump(agg.last30d, sessionId, input, output, cc, cr
|
|
146
|
+
bump(agg.last30d, sessionId, input, output, cc, cr);
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
|
-
function bump(w, sessionId, input, output, cc, cr
|
|
149
|
+
function bump(w, sessionId, input, output, cc, cr) {
|
|
147
150
|
if (sessionId)
|
|
148
151
|
w.sessions.add(sessionId);
|
|
149
152
|
w.input += input;
|
|
150
153
|
w.output += output;
|
|
151
154
|
w.cacheCreate += cc;
|
|
152
155
|
w.cacheRead += cr;
|
|
153
|
-
w.costUsd += cost;
|
|
154
156
|
}
|
|
155
157
|
// ============================================================================
|
|
156
158
|
// 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 —
|
|
1072
|
-
//
|
|
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
|
-
'
|
|
1077
|
-
'
|
|
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
|
|
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
|
|
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
|
-
'
|
|
1146
|
-
'(USD/1M tokens, list price). Aggregation took ' + fmtDuration(cs.elapsedMs) +
|
|
1137
|
+
'Aggregation took ' + fmtDuration(cs.elapsedMs) +
|
|
1147
1138
|
(cs.parseErrors ? ' · ' + cs.parseErrors + ' lines skipped (parse error)' : '') +
|
|
1148
1139
|
'</div>';
|
|
1149
1140
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode a working directory the same way Claude Code does for its
|
|
3
|
+
* `~/.claude/projects/<dir>/` transcript & memory store.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code replaces *every* non-alphanumeric character in the absolute
|
|
6
|
+
* path with `-`. Earlier moflo versions used a narrower class (`/[\\/:]/g`,
|
|
7
|
+
* or split-and-rejoin variants) which agreed with Claude Code only for
|
|
8
|
+
* paths whose every other character was alphanumeric — issue #1048.
|
|
9
|
+
*
|
|
10
|
+
* Centralised here so the stats aggregator and the auto-memory bridge
|
|
11
|
+
* cannot drift apart.
|
|
12
|
+
*/
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
/**
|
|
16
|
+
* Encode a CWD to the form Claude Code uses as a `~/.claude/projects/`
|
|
17
|
+
* subdirectory name. Replaces every non-alphanumeric character with `-`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* encodeCwdForClaudeProjects('C:\\Users\\me\\some_project')
|
|
21
|
+
* → 'C--Users-me-some-project'
|
|
22
|
+
* encodeCwdForClaudeProjects('/Users/me/dev/some_project')
|
|
23
|
+
* → '-Users-me-dev-some-project'
|
|
24
|
+
*/
|
|
25
|
+
export function encodeCwdForClaudeProjects(cwd) {
|
|
26
|
+
return cwd.replace(/[^A-Za-z0-9]/g, '-');
|
|
27
|
+
}
|
|
28
|
+
/** Absolute path to `~/.claude/projects/<encoded-cwd>` for the given CWD. */
|
|
29
|
+
export function claudeProjectDirFor(cwd) {
|
|
30
|
+
return join(homedir(), '.claude', 'projects', encodeCwdForClaudeProjects(cwd));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=claude-projects-path.js.map
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.36",
|
|
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.
|
|
100
|
+
"moflo": "^4.9.35",
|
|
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
|