@mnemom/mnemom 0.17.4-next.4 → 0.17.4-next.5
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.
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Enter / y shows the screen and records acceptance; n skips until the next launch;
|
|
14
14
|
* never stops asking. A non-interactive launch is never asked and writes nothing.
|
|
15
15
|
*/
|
|
16
|
-
import { type Audit } from "./screen.js";
|
|
16
|
+
import { type Audit, type Variation } from "./screen.js";
|
|
17
17
|
export declare const STATE_FILE = "first-run.json";
|
|
18
18
|
export declare const AUDIT_FILE = "first-run-audit.json";
|
|
19
19
|
export declare const OFFER_TEXT: string[];
|
|
@@ -45,6 +45,7 @@ export interface ScanOptions {
|
|
|
45
45
|
export interface FirstRunResult {
|
|
46
46
|
lines: string[];
|
|
47
47
|
audit: Audit;
|
|
48
|
+
variation?: Variation;
|
|
48
49
|
counts: Record<string, number>;
|
|
49
50
|
seconds: number;
|
|
50
51
|
}
|
|
@@ -18,8 +18,8 @@ import { homedir } from "node:os";
|
|
|
18
18
|
import { dirname, join } from "node:path";
|
|
19
19
|
import { estimate, loadTranscript, origContext, ruleRate } from "./backtest.js";
|
|
20
20
|
import { analyzeSeat } from "./seat.js";
|
|
21
|
-
import { authShape, billingClass, buildScope, configDirOf, prettyProject, } from "./scope.js";
|
|
22
|
-
import {
|
|
21
|
+
import { ORG_SEAT_TYPES, authShape, billingClass, buildScope, configDirOf, prettyProject, } from "./scope.js";
|
|
22
|
+
import { DAYS, MAX_LINES, WIDTH, buildScreen, labelFor, } from "./screen.js";
|
|
23
23
|
export const STATE_FILE = "first-run.json";
|
|
24
24
|
export const AUDIT_FILE = "first-run-audit.json";
|
|
25
25
|
export const OFFER_TEXT = [
|
|
@@ -76,11 +76,6 @@ export function recordAnswer(st, ans, now) {
|
|
|
76
76
|
out.accepted_at = iso;
|
|
77
77
|
return out;
|
|
78
78
|
}
|
|
79
|
-
function planName(orgTypes) {
|
|
80
|
-
const names = [...orgTypes].map((t) => (t === "claude_enterprise" ? "Enterprise" : "Team"));
|
|
81
|
-
const uniq = [...new Set(names)].sort();
|
|
82
|
-
return uniq.length === 1 ? uniq[0] : "Team or Enterprise";
|
|
83
|
-
}
|
|
84
79
|
/** Scan, classify, price and build the screen. Read-only on every transcript. */
|
|
85
80
|
export function computeFirstRun(opts = {}) {
|
|
86
81
|
const t0 = Date.now();
|
|
@@ -115,13 +110,13 @@ export function computeFirstRun(opts = {}) {
|
|
|
115
110
|
.map((x) => x.s);
|
|
116
111
|
const seen = new Set();
|
|
117
112
|
const shapes = new Map();
|
|
118
|
-
const
|
|
119
|
-
const
|
|
113
|
+
const sessions = [];
|
|
114
|
+
const subSessions = [];
|
|
115
|
+
// A Team / Enterprise subscriber: a subscription-billed session, or a Team / Enterprise
|
|
116
|
+
// login in any kept session's config dir. Only these plans can run through Mnemom on
|
|
117
|
+
// their subscription (lib/subscription-login.ts), so only they get the usage sentence.
|
|
118
|
+
let orgLogin = false;
|
|
120
119
|
const seatInputs = new Map();
|
|
121
|
-
const orgTypes = new Set();
|
|
122
|
-
const candidates = [];
|
|
123
|
-
let unsupported = 0;
|
|
124
|
-
let unknown = 0;
|
|
125
120
|
ordered.forEach((s, i) => {
|
|
126
121
|
if (i % 25 === 0)
|
|
127
122
|
opts.progress?.(`pricing sessions ${i + 1}/${ordered.length}`);
|
|
@@ -137,58 +132,51 @@ export function computeFirstRun(opts = {}) {
|
|
|
137
132
|
shape = authShape(cfg, home);
|
|
138
133
|
shapes.set(cfg, shape);
|
|
139
134
|
}
|
|
135
|
+
const subLogin = shape.oauth &&
|
|
136
|
+
(shape.billingType ?? "").includes("subscription") &&
|
|
137
|
+
ORG_SEAT_TYPES.has(shape.orgType ?? "");
|
|
140
138
|
const { cls } = billingClass(s, rows, shape);
|
|
141
139
|
counts[`billing_${cls}`] = (counts[`billing_${cls}`] ?? 0) + 1;
|
|
142
|
-
const rowsIn = rows.filter((r) => r.t >= cut);
|
|
143
140
|
if (cls === "subscription_org") {
|
|
144
|
-
//
|
|
145
|
-
orgTypes.add(shape.orgType);
|
|
141
|
+
// Recorded in the audit only: the screen never shows a hit count or overage dollars.
|
|
146
142
|
const acct = seatInputs.get(cfg) ?? { extra: shape.extraUsage, ss: [] };
|
|
147
143
|
acct.ss.push({ rows, rates: rows.map((r) => ruleRate(origContext(r)).screen), paths });
|
|
148
144
|
seatInputs.set(cfg, acct);
|
|
149
145
|
}
|
|
146
|
+
const rowsIn = rows.filter((r) => r.t >= cut);
|
|
150
147
|
if (!rowsIn.length)
|
|
151
148
|
return;
|
|
152
|
-
|
|
153
|
-
unsupported++;
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (cls === "unknown") {
|
|
157
|
-
unknown++;
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
149
|
+
// Every class is priced at API list prices; the screen says so.
|
|
160
150
|
const e = estimate(rowsIn);
|
|
161
|
-
const bucket = cls === "api" ? api : sub;
|
|
162
|
-
bucket.sessions++;
|
|
163
|
-
bucket.actual += e.actual;
|
|
164
|
-
bucket.saved += e.saved;
|
|
165
|
-
// Top + subagent rows are not in one time order: take the true first and last request.
|
|
166
|
-
const start = rowsIn.reduce((m, r) => Math.min(m, r.t), Infinity);
|
|
167
151
|
const end = rowsIn.reduce((m, r) => Math.max(m, r.t), -Infinity);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
152
|
+
const sc = {
|
|
153
|
+
label: labelFor(prettyProject(s.pdir)),
|
|
154
|
+
end,
|
|
155
|
+
actual: e.actual,
|
|
156
|
+
saved: e.saved,
|
|
157
|
+
};
|
|
158
|
+
sessions.push(sc);
|
|
159
|
+
if (cls === "subscription_org")
|
|
160
|
+
subSessions.push(sc);
|
|
161
|
+
orgLogin ||= subLogin;
|
|
178
162
|
});
|
|
179
163
|
const seat = [...seatInputs.values()].map((a) => analyzeSeat(a.ss, a.extra, now));
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const { lines, audit } = buildScreen(
|
|
164
|
+
const use = subSessions.length ? subSessions : sessions;
|
|
165
|
+
const sub = subSessions.length || orgLogin
|
|
166
|
+
? {
|
|
167
|
+
sessions: use.length,
|
|
168
|
+
actual: use.reduce((t, r) => t + r.actual, 0),
|
|
169
|
+
saved: use.reduce((t, r) => t + r.saved, 0),
|
|
170
|
+
}
|
|
171
|
+
: null;
|
|
172
|
+
const { lines, audit, variation } = buildScreen({ sessions, sub });
|
|
189
173
|
assertScreenBudget(lines);
|
|
190
|
-
audit.seat = {
|
|
191
|
-
|
|
174
|
+
audit.seat = {
|
|
175
|
+
shown: null,
|
|
176
|
+
raw: seat,
|
|
177
|
+
method: "seat.analyzeSeat per Team/Enterprise account; recorded, never shown",
|
|
178
|
+
};
|
|
179
|
+
return { lines, audit, variation, counts, seconds: (Date.now() - t0) / 1000 };
|
|
192
180
|
}
|
|
193
181
|
/** The screen's hard budget: a regression in buildScreen's line dropping must fail loudly. */
|
|
194
182
|
export function assertScreenBudget(lines) {
|
|
@@ -4,33 +4,43 @@
|
|
|
4
4
|
* unit-tested. Every number on it is recorded in the audit with its raw value and
|
|
5
5
|
* method.
|
|
6
6
|
*
|
|
7
|
+
* Exactly three variations (operator v1, 2026-10-01); every number has one source and no
|
|
8
|
+
* two lines make the same claim:
|
|
9
|
+
* A saving the shown saving is >= $5 and >= 5%: the usage at API prices, with Mnemom,
|
|
10
|
+
* the saving, the biggest session; plus ONE subscription sentence for a Team /
|
|
11
|
+
* Enterprise subscriber whose usage cut rounds to >= 5%.
|
|
12
|
+
* B small >= $5 of usage at API prices but a smaller saving: the usage, the honest
|
|
13
|
+
* "wouldn't have changed that much" line, and the forward line.
|
|
14
|
+
* C none too little history: the forward line only.
|
|
15
|
+
* All three end with the price line and the live lines.
|
|
16
|
+
*
|
|
7
17
|
* Operator rulings (2026-09-30 / 2026-10-01), binding:
|
|
8
18
|
* - Cost only. Nothing about conduct ("habits", "caught", "checked").
|
|
9
19
|
* - Round DOWN, never overstate; frame the number around the biggest session.
|
|
10
|
-
* - API
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - A saving that rounds below $1 gets the honest "won't change … much" line.
|
|
20
|
+
* - Every kept session is priced at API list prices whatever it was billed on, and the
|
|
21
|
+
* screen says "at API prices". A subscriber gets a usage %, never a seat bill, an
|
|
22
|
+
* overage dollar figure or a limit-hit count.
|
|
14
23
|
* - No live counter is promised: `mnemom agent` has no status bar today.
|
|
15
24
|
*/
|
|
16
|
-
import type { SeatAnalysis } from "./seat.js";
|
|
17
25
|
export declare const WIDTH = 78;
|
|
18
26
|
export declare const MAX_LINES = 16;
|
|
19
|
-
/** Less history than this (
|
|
27
|
+
/** Less history than this (API prices, in the window) is too little to backtest. */
|
|
20
28
|
export declare const MIN_HISTORY_USD = 5;
|
|
21
|
-
/**
|
|
22
|
-
export declare const
|
|
23
|
-
export declare const
|
|
29
|
+
/** Variation A iff the shown saving is at least this many dollars and this many percent. */
|
|
30
|
+
export declare const STRONG_MIN_USD = 5;
|
|
31
|
+
export declare const STRONG_MIN_PCT = 5;
|
|
24
32
|
export declare const DAYS = 30;
|
|
25
33
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
34
|
+
* The forward line: the best finished chain45 run with its error margin (operator
|
|
35
|
+
* 2026-10-01, until something better): S43 C2 total bill -19.5% [-23.9, -14.9], Opus 5.5,
|
|
36
|
+
* n=23, 95% CI rounded to 15-25. The d240p1 ~45% figure is NOT used (its re-bill
|
|
37
|
+
* undercounts output).
|
|
29
38
|
*/
|
|
30
|
-
export declare const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to
|
|
39
|
+
export declare const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to 25% in our tests.";
|
|
40
|
+
/** Prices, promotions and plan allocations move; the saving is in usage (operator 2026-10-01). */
|
|
41
|
+
export declare const PRICE_LINE = "Prices and plan limits change. Mnemom cuts what you use, so you stay ahead.";
|
|
31
42
|
/** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
|
|
32
43
|
export declare const LIVE_LINES: string[];
|
|
33
|
-
export declare const SUPPORTED_LINE = "Mnemom works with an API key or a Claude Team or Enterprise login.";
|
|
34
44
|
/** 2 significant figures, rounded DOWN; 0 below $1. */
|
|
35
45
|
export declare function sig2(x: number): number;
|
|
36
46
|
/** 3 significant figures, rounded DOWN; 0 below $1. */
|
|
@@ -42,41 +52,31 @@ export declare function sig3(x: number): number;
|
|
|
42
52
|
export declare function pair(total: number, saved: number): [number, number, number];
|
|
43
53
|
/** part / whole as a percentage rounded DOWN to a multiple of 5. */
|
|
44
54
|
export declare function pct5(part: number, whole: number): number;
|
|
45
|
-
/** Wall-clock length rounded DOWN: "11 h", "1.5 h", "40 min". */
|
|
46
|
-
export declare function hoursDown(sec: number): string;
|
|
47
55
|
export declare function day(t: number): string;
|
|
48
56
|
export declare function usd(n: number): string;
|
|
49
57
|
export declare function clean(s: string): string;
|
|
50
58
|
/** A short, safe project label from Claude Code's encoded project dir, or null. */
|
|
51
59
|
export declare function labelFor(pretty: string): string | null;
|
|
52
60
|
export declare function wrap(text: string, width?: number): string[];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
/** One kept session's requests in the window, priced at API list prices. */
|
|
62
|
+
export interface SessionCost {
|
|
63
|
+
label: string | null;
|
|
64
|
+
end: number;
|
|
56
65
|
actual: number;
|
|
57
|
-
/** The conservative saving on those requests (backtest.estimate). */
|
|
58
66
|
saved: number;
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
/** A Team / Enterprise subscriber's usage, at API prices (local evidence only). */
|
|
69
|
+
export interface SubUsage {
|
|
70
|
+
/** Subscription-billed sessions in the window (all sessions when none is tagged). */
|
|
71
|
+
sessions: number;
|
|
64
72
|
actual: number;
|
|
65
73
|
saved: number;
|
|
66
|
-
billing: "api" | "subscription";
|
|
67
74
|
}
|
|
68
75
|
export interface ScreenInput {
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
/** Team / Enterprise
|
|
72
|
-
sub:
|
|
73
|
-
plan: string;
|
|
74
|
-
seat: SeatAnalysis[];
|
|
75
|
-
}) | null;
|
|
76
|
-
/** Sessions on a plan the gateway does not support, or with no billing evidence. */
|
|
77
|
-
unsupported: number;
|
|
78
|
-
unknown: number;
|
|
79
|
-
biggest: Biggest | null;
|
|
76
|
+
/** Every kept session with requests in the window. */
|
|
77
|
+
sessions: SessionCost[];
|
|
78
|
+
/** null: not a Team / Enterprise subscriber. */
|
|
79
|
+
sub: SubUsage | null;
|
|
80
80
|
}
|
|
81
81
|
export interface AuditEntry {
|
|
82
82
|
shown: unknown;
|
|
@@ -84,8 +84,10 @@ export interface AuditEntry {
|
|
|
84
84
|
method: string;
|
|
85
85
|
}
|
|
86
86
|
export type Audit = Record<string, AuditEntry>;
|
|
87
|
-
|
|
87
|
+
export type Variation = "A" | "B" | "C";
|
|
88
|
+
/** -> the screen lines, the variation and the audit of every number on them. */
|
|
88
89
|
export declare function buildScreen(inp: ScreenInput): {
|
|
89
90
|
lines: string[];
|
|
90
91
|
audit: Audit;
|
|
92
|
+
variation: Variation;
|
|
91
93
|
};
|
|
@@ -4,38 +4,49 @@
|
|
|
4
4
|
* unit-tested. Every number on it is recorded in the audit with its raw value and
|
|
5
5
|
* method.
|
|
6
6
|
*
|
|
7
|
+
* Exactly three variations (operator v1, 2026-10-01); every number has one source and no
|
|
8
|
+
* two lines make the same claim:
|
|
9
|
+
* A saving the shown saving is >= $5 and >= 5%: the usage at API prices, with Mnemom,
|
|
10
|
+
* the saving, the biggest session; plus ONE subscription sentence for a Team /
|
|
11
|
+
* Enterprise subscriber whose usage cut rounds to >= 5%.
|
|
12
|
+
* B small >= $5 of usage at API prices but a smaller saving: the usage, the honest
|
|
13
|
+
* "wouldn't have changed that much" line, and the forward line.
|
|
14
|
+
* C none too little history: the forward line only.
|
|
15
|
+
* All three end with the price line and the live lines.
|
|
16
|
+
*
|
|
7
17
|
* Operator rulings (2026-09-30 / 2026-10-01), binding:
|
|
8
18
|
* - Cost only. Nothing about conduct ("habits", "caught", "checked").
|
|
9
19
|
* - Round DOWN, never overstate; frame the number around the biggest session.
|
|
10
|
-
* - API
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - A saving that rounds below $1 gets the honest "won't change … much" line.
|
|
20
|
+
* - Every kept session is priced at API list prices whatever it was billed on, and the
|
|
21
|
+
* screen says "at API prices". A subscriber gets a usage %, never a seat bill, an
|
|
22
|
+
* overage dollar figure or a limit-hit count.
|
|
14
23
|
* - No live counter is promised: `mnemom agent` has no status bar today.
|
|
15
24
|
*/
|
|
16
25
|
import { ESTIMATOR_METHOD } from "./backtest.js";
|
|
17
26
|
export const WIDTH = 78;
|
|
18
27
|
export const MAX_LINES = 16;
|
|
19
|
-
/** Less history than this (
|
|
28
|
+
/** Less history than this (API prices, in the window) is too little to backtest. */
|
|
20
29
|
export const MIN_HISTORY_USD = 5;
|
|
21
|
-
/**
|
|
22
|
-
export const
|
|
23
|
-
export const
|
|
30
|
+
/** Variation A iff the shown saving is at least this many dollars and this many percent. */
|
|
31
|
+
export const STRONG_MIN_USD = 5;
|
|
32
|
+
export const STRONG_MIN_PCT = 5;
|
|
24
33
|
export const DAYS = 30;
|
|
25
34
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
35
|
+
* The forward line: the best finished chain45 run with its error margin (operator
|
|
36
|
+
* 2026-10-01, until something better): S43 C2 total bill -19.5% [-23.9, -14.9], Opus 5.5,
|
|
37
|
+
* n=23, 95% CI rounded to 15-25. The d240p1 ~45% figure is NOT used (its re-bill
|
|
38
|
+
* undercounts output).
|
|
29
39
|
*/
|
|
30
|
-
export const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to
|
|
31
|
-
const FORWARD_SOURCE = "
|
|
32
|
-
"
|
|
40
|
+
export const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to 25% in our tests.";
|
|
41
|
+
const FORWARD_SOURCE = "operator 2026-10-01: S43 C2 total bill -19.5% [-23.9, -14.9] (Opus 5.5, n=23), 95% CI " +
|
|
42
|
+
"rounded to 15 to 25; measured vs stock at the 1M default";
|
|
43
|
+
/** Prices, promotions and plan allocations move; the saving is in usage (operator 2026-10-01). */
|
|
44
|
+
export const PRICE_LINE = "Prices and plan limits change. Mnemom cuts what you use, so you stay ahead.";
|
|
33
45
|
/** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
|
|
34
46
|
export const LIVE_LINES = [
|
|
35
47
|
"From now on it's live: your sessions run through Mnemom, which keeps long",
|
|
36
48
|
"conversations lean and puts each tool call your agent makes on the record.",
|
|
37
49
|
];
|
|
38
|
-
export const SUPPORTED_LINE = "Mnemom works with an API key or a Claude Team or Enterprise login.";
|
|
39
50
|
// ── rounding (never inflate) ─────────────────────────────────────────────────
|
|
40
51
|
/** 2 significant figures, rounded DOWN; 0 below $1. */
|
|
41
52
|
export function sig2(x) {
|
|
@@ -66,15 +77,6 @@ export function pct5(part, whole) {
|
|
|
66
77
|
return 0;
|
|
67
78
|
return Math.floor((100 * part) / whole / 5 + 1e-9) * 5;
|
|
68
79
|
}
|
|
69
|
-
/** Wall-clock length rounded DOWN: "11 h", "1.5 h", "40 min". */
|
|
70
|
-
export function hoursDown(sec) {
|
|
71
|
-
const h = sec / 3600;
|
|
72
|
-
if (h >= 2)
|
|
73
|
-
return `${Math.floor(h)} h`;
|
|
74
|
-
if (h >= 1)
|
|
75
|
-
return `${(Math.floor(h * 10) / 10).toFixed(1)} h`;
|
|
76
|
-
return `${Math.max(Math.floor(sec / 60), 1)} min`;
|
|
77
|
-
}
|
|
78
80
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
79
81
|
export function day(t) {
|
|
80
82
|
const d = new Date(t * 1000);
|
|
@@ -115,210 +117,94 @@ export function wrap(text, width = WIDTH) {
|
|
|
115
117
|
out.push(cur);
|
|
116
118
|
return out;
|
|
117
119
|
}
|
|
118
|
-
const SPENT_METHOD = "transcript usage x Claude Code 2.1.285's price table, deduped by message id,
|
|
119
|
-
"
|
|
120
|
+
const SPENT_METHOD = "transcript usage x Claude Code 2.1.285's price table, deduped by message id, every kept " +
|
|
121
|
+
"session's requests in the last 30 days, priced at API list prices whatever they were billed " +
|
|
122
|
+
"on; 3 sig figs DOWN; a lower bound (side calls not in transcripts are missing)";
|
|
120
123
|
const SAVED_METHOD = `backtest.estimate(): ${ESTIMATOR_METHOD}; 2 sig figs DOWN`;
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
const p = pct5(api.saved, api.actual);
|
|
124
|
-
const who = both ? "On your API key you spent" : "You spent";
|
|
125
|
-
A("api_spent_usd", { shown: a, raw: api.actual, method: SPENT_METHOD });
|
|
126
|
-
if (s < 1) {
|
|
127
|
-
A("api_small_saving", { shown: true, raw: api.saved, method: "saving rounds below $1" });
|
|
128
|
-
return wrap(`${who} about ${usd(a)}. Mnemom won't change your bill much.`);
|
|
129
|
-
}
|
|
130
|
-
A("api_saved_usd", { shown: s, raw: api.saved, method: SAVED_METHOD });
|
|
131
|
-
A("api_with_mnemom_usd", {
|
|
132
|
-
shown: e,
|
|
133
|
-
raw: api.actual - api.saved,
|
|
134
|
-
method: "spent shown - saved shown",
|
|
135
|
-
});
|
|
136
|
-
A("api_saved_pct", {
|
|
137
|
-
shown: p,
|
|
138
|
-
raw: (100 * api.saved) / api.actual,
|
|
139
|
-
method: "rounded DOWN to 5s",
|
|
140
|
-
});
|
|
141
|
-
const pctPart = p >= 5 ? `, ${p}%,` : "";
|
|
142
|
-
return [
|
|
143
|
-
...wrap(`${who} about ${usd(a)}. With Mnemom: about ${usd(e)}.`),
|
|
144
|
-
...wrap(`That's about ${usd(s)} back${pctPart} and you don't change how you work.`),
|
|
145
|
-
];
|
|
146
|
-
}
|
|
147
|
-
/** The seat-evidence line: overage paid (real dollars) or limit stops. At most 2 lines. */
|
|
148
|
-
function seatEvidence(seat, A) {
|
|
149
|
-
const ov = seat.flatMap((x) => (x.overage ? [x.overage] : []));
|
|
150
|
-
const paid = ov.reduce((t, o) => t + o.paidUsd, 0);
|
|
151
|
-
if (paid >= 1) {
|
|
152
|
-
const wm = ov.reduce((t, o) => t + o.withMnemomUsd, 0);
|
|
153
|
-
const [p, , w] = pair(paid, paid - wm);
|
|
154
|
-
A("seat_overage_usd", { shown: p, raw: paid, method: ov[0].method });
|
|
155
|
-
A("seat_overage_with_mnemom_usd", { shown: w, raw: wm, method: "paid shown - avoided shown" });
|
|
156
|
-
return wrap(`You paid about ${usd(p)} in extra usage. With Mnemom, about ${usd(w)} of extra usage.`);
|
|
157
|
-
}
|
|
158
|
-
const hs = seat.flatMap((x) => (x.hits ? [x.hits] : []));
|
|
159
|
-
const n = hs.reduce((t, h) => t + h.n, 0);
|
|
160
|
-
if (n < 1)
|
|
124
|
+
function subLines(sub, A) {
|
|
125
|
+
if (!sub)
|
|
161
126
|
return [];
|
|
162
|
-
const wm = hs.reduce((t, h) => t + h.withMnemom, 0);
|
|
163
|
-
const fewer = hs.reduce((t, h) => t + h.notInferableWithSaving, 0);
|
|
164
|
-
const hrs = Math.floor(hs.reduce((t, h) => t + h.waitedS, 0) / 3600);
|
|
165
|
-
A("seat_limit_hits", {
|
|
166
|
-
shown: n,
|
|
167
|
-
raw: n,
|
|
168
|
-
method: "Claude Code's own limit records, last 30 days",
|
|
169
|
-
});
|
|
170
|
-
A("seat_hits_with_mnemom", { shown: wm, raw: wm, method: hs[0].method });
|
|
171
|
-
A("seat_waited_hours", {
|
|
172
|
-
shown: hrs,
|
|
173
|
-
raw: hs.reduce((t, h) => t + h.waitedS, 0) / 3600,
|
|
174
|
-
method: "hours DOWN",
|
|
175
|
-
});
|
|
176
|
-
const wait = hrs >= 1 ? ` and waited about ${hrs} hour${hrs === 1 ? "" : "s"}` : "";
|
|
177
|
-
let tail = "";
|
|
178
|
-
if (wm < n)
|
|
179
|
-
tail = ` With Mnemom, about ${wm}.`;
|
|
180
|
-
else if (fewer)
|
|
181
|
-
tail = " With Mnemom, fewer.";
|
|
182
|
-
return wrap(`You hit your plan's usage limit ${n} time${n === 1 ? "" : "s"}${wait}.${tail}`);
|
|
183
|
-
}
|
|
184
|
-
function subBlock(sub, A) {
|
|
185
|
-
const [u, s, w] = pair(sub.actual, sub.saved);
|
|
186
127
|
const p = pct5(sub.saved, sub.actual);
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
shown:
|
|
190
|
-
|
|
191
|
-
method: `${SPENT_METHOD}; list-price equivalent, not a bill`,
|
|
192
|
-
});
|
|
193
|
-
const head = wrap(`On your Claude ${sub.plan} plan: ${n} session${n === 1 ? "" : "s"}, about ${usd(u)} of usage at API list price.`);
|
|
194
|
-
if (s < 1) {
|
|
195
|
-
A("sub_small_saving", { shown: true, raw: sub.saved, method: "saving rounds below $1" });
|
|
196
|
-
return { lines: [...head, "Mnemom won't change your usage much."], evidence: [] };
|
|
128
|
+
const raw = sub.actual > 0 ? (100 * sub.saved) / sub.actual : 0;
|
|
129
|
+
if (p < 5) {
|
|
130
|
+
A("sub_line_omitted", { shown: true, raw, method: "usage cut rounds DOWN below 5%" });
|
|
131
|
+
return [];
|
|
197
132
|
}
|
|
198
|
-
A("
|
|
199
|
-
A("sub_with_mnemom_list_usd", {
|
|
200
|
-
shown: w,
|
|
201
|
-
raw: sub.actual - sub.saved,
|
|
202
|
-
method: "usage shown - saved shown",
|
|
203
|
-
});
|
|
204
|
-
// Using a fraction f less per request fits 1/(1-f) - 1 >= f more work in a window; we
|
|
205
|
-
// state f itself, rounded down to 5s, which never exceeds the true headroom.
|
|
206
|
-
A("sub_headroom_pct", {
|
|
133
|
+
A("sub_usage_pct", {
|
|
207
134
|
shown: p,
|
|
208
|
-
raw
|
|
209
|
-
method: "
|
|
210
|
-
"
|
|
135
|
+
raw,
|
|
136
|
+
method: "saving / API-price cost over the subscription sessions in the window (all sessions if " +
|
|
137
|
+
"none is tagged); cost-weighted, rounded DOWN to 5s",
|
|
211
138
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
: "a little more room in each usage window before a limit.";
|
|
215
|
-
return {
|
|
216
|
-
lines: [
|
|
217
|
-
...head,
|
|
218
|
-
...wrap(`With Mnemom: about ${usd(w)} at list price (not your bill), so ${room}`),
|
|
219
|
-
],
|
|
220
|
-
evidence: seatEvidence(sub.seat, A),
|
|
221
|
-
};
|
|
139
|
+
return wrap(`On your subscription that is about ${p}% less usage: fewer limit hits, and less ` +
|
|
140
|
+
"overage if you use extra usage.");
|
|
222
141
|
}
|
|
223
|
-
|
|
224
|
-
const [a, , e] = pair(b.actual, b.saved);
|
|
225
|
-
const hrs = hoursDown(b.end - b.start);
|
|
226
|
-
const lp = b.billing === "subscription" ? " at list price" : "";
|
|
227
|
-
A("biggest_session_usd", {
|
|
228
|
-
shown: a,
|
|
229
|
-
raw: b.actual,
|
|
230
|
-
method: "largest bill among sessions >= 30 min with >= $5 saved; 3 sig figs DOWN",
|
|
231
|
-
});
|
|
232
|
-
A("biggest_session_with_mnemom_usd", {
|
|
233
|
-
shown: e,
|
|
234
|
-
raw: b.actual - b.saved,
|
|
235
|
-
method: "its bill shown - its saving (2 sig figs DOWN)",
|
|
236
|
-
});
|
|
237
|
-
A("biggest_session_hours", {
|
|
238
|
-
shown: hrs,
|
|
239
|
-
raw: (b.end - b.start) / 3600,
|
|
240
|
-
method: "first to last request, wall clock; DOWN",
|
|
241
|
-
});
|
|
242
|
-
const head = b.label
|
|
243
|
-
? `Your biggest session: ${b.label}, ${day(b.end)}, ${hrs}, ${usd(a)}${lp}.`
|
|
244
|
-
: `Your biggest session, on ${day(b.end)}: ${hrs}, ${usd(a)}${lp}.`;
|
|
245
|
-
const tail = b.billing === "subscription"
|
|
246
|
-
? `With Mnemom it would have used about ${usd(e)} of that.`
|
|
247
|
-
: `With Mnemom it would have cost about ${usd(e)}.`;
|
|
248
|
-
return [...wrap(head), tail];
|
|
249
|
-
}
|
|
250
|
-
/** -> the screen lines and the audit of every number on them. */
|
|
142
|
+
/** -> the screen lines, the variation and the audit of every number on them. */
|
|
251
143
|
export function buildScreen(inp) {
|
|
252
144
|
const audit = {};
|
|
253
145
|
const A = (k, e) => {
|
|
254
146
|
audit[k] = e;
|
|
255
147
|
};
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
raw: [15.7, 15.8, 19.5],
|
|
293
|
-
method: FORWARD_SOURCE,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
148
|
+
const ses = inp.sessions;
|
|
149
|
+
const actual = ses.reduce((t, r) => t + r.actual, 0);
|
|
150
|
+
const saved = ses.reduce((t, r) => t + r.saved, 0);
|
|
151
|
+
const [a, s, e] = pair(actual, saved);
|
|
152
|
+
const p = pct5(saved, actual);
|
|
153
|
+
const enough = actual >= MIN_HISTORY_USD;
|
|
154
|
+
const variation = enough && s >= STRONG_MIN_USD && p >= STRONG_MIN_PCT ? "A" : enough ? "B" : "C";
|
|
155
|
+
A("variation", { shown: variation, raw: { actual, saved }, method: "see screen.ts header" });
|
|
156
|
+
const L = ["Welcome to Mnemom.", ""];
|
|
157
|
+
L.push(...(variation === "C"
|
|
158
|
+
? wrap("We didn't find enough Claude Code history on this machine to look at.")
|
|
159
|
+
: wrap(`We looked at your last ${DAYS} days of Claude Code. It all stayed on this machine.`)));
|
|
160
|
+
L.push("");
|
|
161
|
+
if (variation === "A") {
|
|
162
|
+
A("spent_usd", { shown: a, raw: actual, method: SPENT_METHOD });
|
|
163
|
+
A("saved_usd", { shown: s, raw: saved, method: SAVED_METHOD });
|
|
164
|
+
A("with_mnemom_usd", { shown: e, raw: actual - saved, method: "spent shown - saved shown" });
|
|
165
|
+
A("saved_pct", { shown: p, raw: (100 * saved) / actual, method: "rounded DOWN to 5s" });
|
|
166
|
+
L.push(...wrap(`At API prices that usage cost about ${usd(a)}. With Mnemom: about ${usd(e)}.`));
|
|
167
|
+
L.push(...wrap(`That's about ${usd(s)} less (${p}%), and you don't change how you work.`));
|
|
168
|
+
// A has a saving, so the session with the largest saving exists and saved something.
|
|
169
|
+
const big = ses.reduce((m, r) => (r.saved > m.saved ? r : m));
|
|
170
|
+
const [ba, , be] = pair(big.actual, big.saved);
|
|
171
|
+
A("biggest_session_usd", {
|
|
172
|
+
shown: ba,
|
|
173
|
+
raw: big.actual,
|
|
174
|
+
method: "the session with the largest saving; its API-price cost, 3 sig figs DOWN",
|
|
175
|
+
});
|
|
176
|
+
A("biggest_session_with_mnemom_usd", {
|
|
177
|
+
shown: be,
|
|
178
|
+
raw: big.actual - big.saved,
|
|
179
|
+
method: "its cost shown - its saving (2 sig figs DOWN)",
|
|
180
|
+
});
|
|
181
|
+
const where = big.label ? `${big.label} on ${day(big.end)}` : `on ${day(big.end)}`;
|
|
182
|
+
L.push(...wrap(`Your biggest session, ${where}: ${usd(ba)}, with Mnemom about ${usd(be)}.`));
|
|
183
|
+
L.push(...subLines(inp.sub, A));
|
|
296
184
|
}
|
|
297
185
|
else {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
186
|
+
if (variation === "B") {
|
|
187
|
+
A("spent_usd", { shown: a, raw: actual, method: SPENT_METHOD });
|
|
188
|
+
A("saved_usd_not_shown", {
|
|
189
|
+
shown: null,
|
|
190
|
+
raw: saved,
|
|
191
|
+
method: `below $${STRONG_MIN_USD} or ${STRONG_MIN_PCT}% after rounding DOWN`,
|
|
192
|
+
});
|
|
193
|
+
L.push(...wrap(`Your sessions used about ${usd(a)} at API prices. They stayed small, so Mnemom ` +
|
|
194
|
+
"wouldn't have changed that much."));
|
|
195
|
+
}
|
|
196
|
+
L.push(FORWARD_LINE);
|
|
197
|
+
A("forward_range_pct", {
|
|
198
|
+
shown: "15 to 25",
|
|
199
|
+
raw: [-23.9, -19.5, -14.9],
|
|
200
|
+
method: FORWARD_SOURCE,
|
|
201
|
+
});
|
|
301
202
|
}
|
|
302
|
-
push(LIVE_LINES);
|
|
203
|
+
L.push("", PRICE_LINE, "", ...LIVE_LINES);
|
|
303
204
|
A("sessions", {
|
|
304
205
|
shown: null,
|
|
305
|
-
raw:
|
|
306
|
-
|
|
307
|
-
subscription: inp.sub?.sessions ?? 0,
|
|
308
|
-
unsupported: inp.unsupported,
|
|
309
|
-
unknown: inp.unknown,
|
|
310
|
-
},
|
|
311
|
-
method: "kept (ungoverned) sessions with requests in the window, by billing class",
|
|
206
|
+
raw: ses.length,
|
|
207
|
+
method: "kept sessions with requests in the window",
|
|
312
208
|
});
|
|
313
|
-
|
|
314
|
-
for (const level of [3, 2]) {
|
|
315
|
-
if (count() <= MAX_LINES)
|
|
316
|
-
break;
|
|
317
|
-
for (let i = sections.length - 1; i >= 0; i--) {
|
|
318
|
-
if (sections[i].drop === level)
|
|
319
|
-
sections.splice(i, 1);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const lines = sections.flatMap((s) => s.lines).map(clean);
|
|
323
|
-
return { lines, audit };
|
|
209
|
+
return { lines: L.map(clean), audit, variation };
|
|
324
210
|
}
|