@mnemom/mnemom 0.17.4-next.3 → 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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Which local Claude Code sessions the first-run look may count, and how each was billed.
3
+ *
4
+ * Operator rulings (2026-09-30 / 2026-10-01), binding:
5
+ * - The ONLY exclusion is "governed" (already ran through Mnemom); when in doubt,
6
+ * exclude. A headless `claude -p` session (entrypoint sdk-cli) is excluded: the env of
7
+ * the script that launched it (ANTHROPIC_BASE_URL, a proxy) is never recorded.
8
+ * - Sessions are discovered in ~/.claude/projects and every ~/.claude-*\/projects.
9
+ * - Dollars are shown for API-billed sessions; Team / Enterprise subscription sessions
10
+ * (the plans `mnemom agent --auth subscription` supports) are in scope as usage-limit
11
+ * headroom with a clearly labelled list-price equivalent. Pro / Max are not supported
12
+ * by the gateway (lib/subscription-login.ts), so they are never given a figure.
13
+ *
14
+ * Read-only on every config dir. No network. Reads only the auth SHAPE of .claude.json
15
+ * (oauthAccount.{organizationType, seatTier, billingType, organizationUuid,
16
+ * hasExtraUsageEnabled} and the COUNT of approved API keys) — never a secret or identity.
17
+ */
18
+ import type { Row } from "./backtest.js";
19
+ import { Governance } from "./governed.js";
20
+ export interface Session {
21
+ key: string;
22
+ sid: string;
23
+ /** The projects/ dir the session lives in. */
24
+ root: string;
25
+ /** The project dir name (Claude Code's encoded cwd). */
26
+ pdir: string;
27
+ top: string | null;
28
+ subs: string[];
29
+ cwd?: string | null;
30
+ entry?: string | null;
31
+ firstTs?: number | null;
32
+ }
33
+ export type BillingClass = "api" | "subscription_org" | "subscription_consumer" | "subscription_unknown_type" | "unknown";
34
+ export interface ScopeResult {
35
+ roots: string[];
36
+ sessions: Map<string, Session>;
37
+ kept: Map<string, Session>;
38
+ excluded: Map<string, string[]>;
39
+ }
40
+ /** ~/.claude/projects and ~/.claude-*\/projects that exist (realpath, de-duplicated). */
41
+ export declare function defaultProjectRoots(home?: string): string[];
42
+ /** Every top-level transcript and subagent transcript under the roots, grouped by session. */
43
+ export declare function discover(roots: readonly string[]): Map<string, Session>;
44
+ export interface ClassifyOptions {
45
+ home?: string;
46
+ /** Exclude sessions that started before this (epoch s): the ledger cannot vouch for them. */
47
+ notBefore?: number | null;
48
+ progress?: (msg: string) => void;
49
+ }
50
+ /** Newest mtime (epoch s) across a session's files; 0 when none can be read. */
51
+ export declare function newestMtime(s: Session): number;
52
+ /** Only sessions with a file written at or after `since` can hold a request in the window. */
53
+ export declare function touchedSince(sessions: Map<string, Session>, since: number): Map<string, Session>;
54
+ /**
55
+ * Kept iff NO governed signal on any of the session's files and nothing makes us unsure.
56
+ * The same session id in several roots: if any copy is excluded, all are; else one is kept.
57
+ */
58
+ export declare function classify(sessions: Map<string, Session>, gov: Governance, opts?: ClassifyOptions): {
59
+ kept: Map<string, Session>;
60
+ excluded: Map<string, string[]>;
61
+ };
62
+ /** organizationType values in Claude Code 2.1.285 (binary map: claude_team → team, …). */
63
+ export declare const ORG_SEAT_TYPES: Set<string>;
64
+ export declare const CONSUMER_TYPES: Set<string>;
65
+ export interface AuthShape {
66
+ found: boolean;
67
+ oauth: boolean;
68
+ billingType: string | null;
69
+ orgType: string | null;
70
+ seat: string | null;
71
+ orgUuid: string | null;
72
+ extraUsage: boolean | null;
73
+ approvedKeys: number;
74
+ }
75
+ /** The auth SHAPE of a config dir's .claude.json (~/.claude → ~/.claude.json). */
76
+ export declare function authShape(configDir: string, home?: string): AuthShape;
77
+ /**
78
+ * How a session was billed, from evidence only:
79
+ * - Cache-TTL signature (verified in Claude Code 2.1.285): without an override the main
80
+ * thread writes 1-hour cache only under a claude.ai subscription login (reason
81
+ * "subscriber"); an API key gets 5m. A main-thread 1h write + a subscription login =
82
+ * subscription. 5m never proves API (a subscriber in overage also gets 5m).
83
+ * - No login in the config dir + approved API key(s) = api.
84
+ * - Anything else = unknown (no figure).
85
+ * The subscription KIND is the current login's ("current account, assumed"), except a
86
+ * Remote Control record naming a different organization, which makes it unknown.
87
+ */
88
+ export declare function billingClass(session: Session, rows: readonly Row[], shape: AuthShape): {
89
+ cls: BillingClass;
90
+ why: string;
91
+ };
92
+ /** The Claude config dir holding a projects/ root. */
93
+ export declare function configDirOf(root: string): string;
94
+ /** A readable project label: the encoded cwd without the home prefix. */
95
+ export declare function prettyProject(pdir: string): string;
96
+ export interface BuildOptions extends ClassifyOptions {
97
+ roots?: string[];
98
+ mnemomDir?: string;
99
+ tmp?: string;
100
+ /** Only sessions with a file written at or after this (epoch s) are looked at. */
101
+ since?: number;
102
+ /** Ledger retention in days: a session older than that cannot be vouched for. */
103
+ retentionDays?: number;
104
+ now?: number;
105
+ }
106
+ export declare function buildScope(opts?: BuildOptions): ScopeResult & {
107
+ inWindow: number;
108
+ };
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Which local Claude Code sessions the first-run look may count, and how each was billed.
3
+ *
4
+ * Operator rulings (2026-09-30 / 2026-10-01), binding:
5
+ * - The ONLY exclusion is "governed" (already ran through Mnemom); when in doubt,
6
+ * exclude. A headless `claude -p` session (entrypoint sdk-cli) is excluded: the env of
7
+ * the script that launched it (ANTHROPIC_BASE_URL, a proxy) is never recorded.
8
+ * - Sessions are discovered in ~/.claude/projects and every ~/.claude-*\/projects.
9
+ * - Dollars are shown for API-billed sessions; Team / Enterprise subscription sessions
10
+ * (the plans `mnemom agent --auth subscription` supports) are in scope as usage-limit
11
+ * headroom with a clearly labelled list-price equivalent. Pro / Max are not supported
12
+ * by the gateway (lib/subscription-login.ts), so they are never given a figure.
13
+ *
14
+ * Read-only on every config dir. No network. Reads only the auth SHAPE of .claude.json
15
+ * (oauthAccount.{organizationType, seatTier, billingType, organizationUuid,
16
+ * hasExtraUsageEnabled} and the COUNT of approved API keys) — never a secret or identity.
17
+ */
18
+ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { basename, dirname, join } from "node:path";
21
+ import { Governance } from "./governed.js";
22
+ import { parseJson } from "./lines.js";
23
+ function realOrNull(p) {
24
+ try {
25
+ return realpathSync(p);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function isDir(p) {
32
+ try {
33
+ return statSync(p).isDirectory();
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ function list(dir) {
40
+ try {
41
+ return readdirSync(dir).sort();
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ /** ~/.claude/projects and ~/.claude-*\/projects that exist (realpath, de-duplicated). */
48
+ export function defaultProjectRoots(home = homedir()) {
49
+ const out = [];
50
+ const add = (p) => {
51
+ const rp = realOrNull(p);
52
+ if (rp && isDir(rp) && !out.includes(rp))
53
+ out.push(rp);
54
+ };
55
+ add(join(home, ".claude", "projects"));
56
+ for (const d of list(home)) {
57
+ if (d.startsWith(".claude-"))
58
+ add(join(home, d, "projects"));
59
+ }
60
+ return out;
61
+ }
62
+ /** Every top-level transcript and subagent transcript under the roots, grouped by session. */
63
+ export function discover(roots) {
64
+ const sessions = new Map();
65
+ const get = (root, pdir, sid) => {
66
+ const key = `${root}\u0000${pdir}\u0000${sid}`;
67
+ let s = sessions.get(key);
68
+ if (!s) {
69
+ s = { key, sid, root, pdir, top: null, subs: [] };
70
+ sessions.set(key, s);
71
+ }
72
+ return s;
73
+ };
74
+ for (const root of roots) {
75
+ for (const pdir of list(root)) {
76
+ // A stray file under the root lists as empty (list() never throws).
77
+ const full = join(root, pdir);
78
+ for (const f of list(full)) {
79
+ const p = join(full, f);
80
+ if (f.endsWith(".jsonl")) {
81
+ get(root, pdir, f.slice(0, -6)).top = p;
82
+ }
83
+ else if (isDir(join(p, "subagents"))) {
84
+ for (const sub of walkJsonl(join(p, "subagents")))
85
+ get(root, pdir, f).subs.push(sub);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return sessions;
91
+ }
92
+ function walkJsonl(dir, depth = 0) {
93
+ const out = [];
94
+ if (depth > 4)
95
+ return out;
96
+ for (const f of list(dir)) {
97
+ const p = join(dir, f);
98
+ if (f.endsWith(".jsonl"))
99
+ out.push(p);
100
+ else if (isDir(p))
101
+ out.push(...walkJsonl(p, depth + 1));
102
+ }
103
+ return out;
104
+ }
105
+ /** Newest mtime (epoch s) across a session's files; 0 when none can be read. */
106
+ export function newestMtime(s) {
107
+ let m = 0;
108
+ for (const p of [s.top, ...s.subs]) {
109
+ if (!p)
110
+ continue;
111
+ try {
112
+ m = Math.max(m, statSync(p).mtimeMs / 1000);
113
+ }
114
+ catch {
115
+ /* unreadable: ignored */
116
+ }
117
+ }
118
+ return m;
119
+ }
120
+ /** Only sessions with a file written at or after `since` can hold a request in the window. */
121
+ export function touchedSince(sessions, since) {
122
+ const out = new Map();
123
+ for (const [k, s] of sessions)
124
+ if (newestMtime(s) >= since)
125
+ out.set(k, s);
126
+ return out;
127
+ }
128
+ /**
129
+ * Kept iff NO governed signal on any of the session's files and nothing makes us unsure.
130
+ * The same session id in several roots: if any copy is excluded, all are; else one is kept.
131
+ */
132
+ export function classify(sessions, gov, opts = {}) {
133
+ const home = opts.home ?? homedir();
134
+ const kept = new Map();
135
+ const excluded = new Map();
136
+ const bySid = new Map();
137
+ let i = 0;
138
+ for (const [key, s] of sessions) {
139
+ if (++i % 200 === 0)
140
+ opts.progress?.(`checking sessions ${i}/${sessions.size}`);
141
+ if (!s.top) {
142
+ excluded.set(key, ["no_parent_transcript"]);
143
+ continue;
144
+ }
145
+ const sigs = new Set();
146
+ const top = gov.classify(s.top);
147
+ top.signals.forEach((x) => sigs.add(x));
148
+ s.cwd = top.cwd;
149
+ s.entry = top.entrypoint;
150
+ s.firstTs = top.firstTs;
151
+ if (sigs.size === 0) {
152
+ for (const sub of s.subs) {
153
+ gov.classify(sub).signals.forEach((x) => sigs.add(x));
154
+ if (sigs.size)
155
+ break;
156
+ }
157
+ }
158
+ if (top.entrypoint === "sdk-cli")
159
+ sigs.add("headless_unproven");
160
+ if (top.cwd && top.cwd.startsWith(join(home, ".pegasus") + "/")) {
161
+ sigs.add("pegasus_host_worktree");
162
+ }
163
+ if (opts.notBefore != null && (top.firstTs === null || top.firstTs < opts.notBefore)) {
164
+ sigs.add("before_ledger_retention");
165
+ }
166
+ const keys = bySid.get(s.sid) ?? [];
167
+ keys.push(key);
168
+ bySid.set(s.sid, keys);
169
+ if (sigs.size)
170
+ excluded.set(key, [...sigs].sort());
171
+ else
172
+ kept.set(key, s);
173
+ }
174
+ for (const keys of bySid.values()) {
175
+ if (keys.length < 2)
176
+ continue;
177
+ if (keys.some((k) => excluded.has(k))) {
178
+ for (const k of keys) {
179
+ if (kept.delete(k))
180
+ excluded.set(k, ["duplicate_of_excluded_copy"]);
181
+ }
182
+ }
183
+ else {
184
+ for (const k of keys.slice(1)) {
185
+ kept.delete(k);
186
+ excluded.set(k, ["duplicate_copy"]);
187
+ }
188
+ }
189
+ }
190
+ return { kept, excluded };
191
+ }
192
+ // ── billing ──────────────────────────────────────────────────────────────────
193
+ /** organizationType values in Claude Code 2.1.285 (binary map: claude_team → team, …). */
194
+ export const ORG_SEAT_TYPES = new Set(["claude_team", "claude_enterprise"]);
195
+ export const CONSUMER_TYPES = new Set(["claude_pro", "claude_max"]);
196
+ /** The auth SHAPE of a config dir's .claude.json (~/.claude → ~/.claude.json). */
197
+ export function authShape(configDir, home = homedir()) {
198
+ const isDefault = realOrNull(configDir) === realOrNull(join(home, ".claude"));
199
+ const p = isDefault ? join(home, ".claude.json") : join(configDir, ".claude.json");
200
+ let d;
201
+ try {
202
+ d = parseJson(readFileSync(p, "utf8"));
203
+ }
204
+ catch {
205
+ d = null;
206
+ }
207
+ if (!d) {
208
+ return {
209
+ found: false,
210
+ oauth: false,
211
+ billingType: null,
212
+ orgType: null,
213
+ seat: null,
214
+ orgUuid: null,
215
+ extraUsage: null,
216
+ approvedKeys: 0,
217
+ };
218
+ }
219
+ const oa = d.oauthAccount ?? {};
220
+ const str = (v) => (typeof v === "string" ? v : null);
221
+ const approved = d.customApiKeyResponses?.approved;
222
+ return {
223
+ found: true,
224
+ oauth: Object.keys(oa).length > 0,
225
+ billingType: str(oa.billingType),
226
+ orgType: str(oa.organizationType),
227
+ seat: str(oa.seatTier),
228
+ orgUuid: str(oa.organizationUuid),
229
+ extraUsage: typeof oa.hasExtraUsageEnabled === "boolean" ? oa.hasExtraUsageEnabled : null,
230
+ approvedKeys: Array.isArray(approved) ? approved.length : 0,
231
+ };
232
+ }
233
+ const RC_ORG = /"ownerOrganizationUuid"\s*:\s*"([0-9a-f-]{36})"/;
234
+ function rcOrg(top) {
235
+ let text;
236
+ try {
237
+ text = readFileSync(top, "utf8");
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ const m = RC_ORG.exec(text);
243
+ return m ? m[1] : null;
244
+ }
245
+ /**
246
+ * How a session was billed, from evidence only:
247
+ * - Cache-TTL signature (verified in Claude Code 2.1.285): without an override the main
248
+ * thread writes 1-hour cache only under a claude.ai subscription login (reason
249
+ * "subscriber"); an API key gets 5m. A main-thread 1h write + a subscription login =
250
+ * subscription. 5m never proves API (a subscriber in overage also gets 5m).
251
+ * - No login in the config dir + approved API key(s) = api.
252
+ * - Anything else = unknown (no figure).
253
+ * The subscription KIND is the current login's ("current account, assumed"), except a
254
+ * Remote Control record naming a different organization, which makes it unknown.
255
+ */
256
+ export function billingClass(session, rows, shape) {
257
+ const main1h = rows.some((r) => r.cw1h > 0 && r.mainThread && !r.subagent);
258
+ if (shape.oauth && (shape.billingType ?? "").includes("subscription")) {
259
+ if (!main1h) {
260
+ return {
261
+ cls: "unknown",
262
+ why: "subscription login; no per-session evidence of how it was billed",
263
+ };
264
+ }
265
+ const cls = ORG_SEAT_TYPES.has(shape.orgType ?? "")
266
+ ? "subscription_org"
267
+ : CONSUMER_TYPES.has(shape.orgType ?? "")
268
+ ? "subscription_consumer"
269
+ : "subscription_unknown_type";
270
+ const rc = session.top ? rcOrg(session.top) : null;
271
+ if (rc && shape.orgUuid && rc !== shape.orgUuid) {
272
+ return { cls: "unknown", why: "Remote Control record names a different organization" };
273
+ }
274
+ const tie = rc && shape.orgUuid ? "Remote Control record matches the login" : "current account, assumed";
275
+ return {
276
+ cls,
277
+ why: `subscription login (${shape.orgType ?? "unknown type"}) and 1-hour main-thread cache writes; ${tie}`,
278
+ };
279
+ }
280
+ if (!shape.oauth && shape.approvedKeys > 0) {
281
+ return { cls: "api", why: "no subscription login in this config dir; approved API key(s)" };
282
+ }
283
+ return { cls: "unknown", why: "no per-session evidence of how it was billed" };
284
+ }
285
+ /** The Claude config dir holding a projects/ root. */
286
+ export function configDirOf(root) {
287
+ return dirname(root);
288
+ }
289
+ /** A readable project label: the encoded cwd without the home prefix. */
290
+ export function prettyProject(pdir) {
291
+ const n = pdir.replace(/^-+/, "");
292
+ const stripped = n.replace(/^Users-[^-]+-(?:git-)?/, "").replace(/^home-[^-]+-(?:git-)?/, "");
293
+ return stripped || basename(pdir);
294
+ }
295
+ export function buildScope(opts = {}) {
296
+ const home = opts.home ?? homedir();
297
+ const now = opts.now ?? Date.now() / 1000;
298
+ const roots = opts.roots ?? defaultProjectRoots(home);
299
+ const all = discover(roots);
300
+ const sessions = opts.since != null ? touchedSince(all, opts.since) : all;
301
+ opts.progress?.(`found ${sessions.size} recent sessions`);
302
+ const gov = new Governance({ home, mnemomDir: opts.mnemomDir, tmp: opts.tmp });
303
+ // With a ledger, anything older than its retention may have been pruned from it.
304
+ const notBefore = opts.notBefore ??
305
+ (gov.registryRows > 0 && opts.retentionDays ? now - opts.retentionDays * 86400 : null);
306
+ const { kept, excluded } = classify(sessions, gov, { ...opts, home, notBefore });
307
+ return { roots, sessions: all, kept, excluded, inWindow: sessions.size };
308
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The first-run screen: one plain terminal screen (at most 16 lines of at most 78
3
+ * columns, ASCII) built from the backtest numbers. Pure — no I/O — so every layout is
4
+ * unit-tested. Every number on it is recorded in the audit with its raw value and
5
+ * method.
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
+ *
17
+ * Operator rulings (2026-09-30 / 2026-10-01), binding:
18
+ * - Cost only. Nothing about conduct ("habits", "caught", "checked").
19
+ * - Round DOWN, never overstate; frame the number around the biggest session.
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.
23
+ * - No live counter is promised: `mnemom agent` has no status bar today.
24
+ */
25
+ export declare const WIDTH = 78;
26
+ export declare const MAX_LINES = 16;
27
+ /** Less history than this (API prices, in the window) is too little to backtest. */
28
+ export declare const MIN_HISTORY_USD = 5;
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;
32
+ export declare const DAYS = 30;
33
+ /**
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).
38
+ */
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.";
42
+ /** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
43
+ export declare const LIVE_LINES: string[];
44
+ /** 2 significant figures, rounded DOWN; 0 below $1. */
45
+ export declare function sig2(x: number): number;
46
+ /** 3 significant figures, rounded DOWN; 0 below $1. */
47
+ export declare function sig3(x: number): number;
48
+ /**
49
+ * (total shown, saved shown, with-Mnemom shown) with total - saved === with EXACTLY on
50
+ * screen. The implied saving is the shown saving, which never exceeds the raw saving.
51
+ */
52
+ export declare function pair(total: number, saved: number): [number, number, number];
53
+ /** part / whole as a percentage rounded DOWN to a multiple of 5. */
54
+ export declare function pct5(part: number, whole: number): number;
55
+ export declare function day(t: number): string;
56
+ export declare function usd(n: number): string;
57
+ export declare function clean(s: string): string;
58
+ /** A short, safe project label from Claude Code's encoded project dir, or null. */
59
+ export declare function labelFor(pretty: string): string | null;
60
+ export declare function wrap(text: string, width?: number): string[];
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;
65
+ actual: number;
66
+ saved: number;
67
+ }
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;
72
+ actual: number;
73
+ saved: number;
74
+ }
75
+ export interface ScreenInput {
76
+ /** Every kept session with requests in the window. */
77
+ sessions: SessionCost[];
78
+ /** null: not a Team / Enterprise subscriber. */
79
+ sub: SubUsage | null;
80
+ }
81
+ export interface AuditEntry {
82
+ shown: unknown;
83
+ raw: unknown;
84
+ method: string;
85
+ }
86
+ export type Audit = Record<string, AuditEntry>;
87
+ export type Variation = "A" | "B" | "C";
88
+ /** -> the screen lines, the variation and the audit of every number on them. */
89
+ export declare function buildScreen(inp: ScreenInput): {
90
+ lines: string[];
91
+ audit: Audit;
92
+ variation: Variation;
93
+ };