@mnemom/mnemom 0.17.4-next.3 → 0.17.4-next.4

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,91 @@
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
+ * Operator rulings (2026-09-30 / 2026-10-01), binding:
8
+ * - Cost only. Nothing about conduct ("habits", "caught", "checked").
9
+ * - Round DOWN, never overstate; frame the number around the biggest session.
10
+ * - API-key sessions: dollars. Team / Enterprise subscription sessions: usage-limit
11
+ * headroom (more work per window, fewer limit stops, less overage) plus the saving
12
+ * at API list price, labelled as such — never a lower seat bill.
13
+ * - A saving that rounds below $1 gets the honest "won't change … much" line.
14
+ * - No live counter is promised: `mnemom agent` has no status bar today.
15
+ */
16
+ import type { SeatAnalysis } from "./seat.js";
17
+ export declare const WIDTH = 78;
18
+ export declare const MAX_LINES = 16;
19
+ /** Less history than this (list price, in the window) is too little to backtest. */
20
+ export declare const MIN_HISTORY_USD = 5;
21
+ /** The biggest session is shown only if it ran this long and saved at least this much. */
22
+ export declare const BIGGEST_MIN_SECONDS = 1800;
23
+ export declare const BIGGEST_MIN_SAVED_USD = 5;
24
+ export declare const DAYS = 30;
25
+ /**
26
+ * Source: mnemom-adw scratchpad/wow-first-run/COST-REPORT.md, measured chain45 total
27
+ * bill (S44 C2 fold-only -15.8%, S43 C2 -19.5%, S43 C1 -15.7%), stated as the rounded-down
28
+ * range. The d240p1 ~45% figure is NOT used (its re-bill undercounts output).
29
+ */
30
+ export declare const FORWARD_LINE = "Long sessions are where Mnemom saves the most: about 15 to 20% in our tests.";
31
+ /** What `mnemom agent` does today: routes through the gateway, records, keeps context lean. */
32
+ 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
+ /** 2 significant figures, rounded DOWN; 0 below $1. */
35
+ export declare function sig2(x: number): number;
36
+ /** 3 significant figures, rounded DOWN; 0 below $1. */
37
+ export declare function sig3(x: number): number;
38
+ /**
39
+ * (total shown, saved shown, with-Mnemom shown) with total - saved === with EXACTLY on
40
+ * screen. The implied saving is the shown saving, which never exceeds the raw saving.
41
+ */
42
+ export declare function pair(total: number, saved: number): [number, number, number];
43
+ /** part / whole as a percentage rounded DOWN to a multiple of 5. */
44
+ 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
+ export declare function day(t: number): string;
48
+ export declare function usd(n: number): string;
49
+ export declare function clean(s: string): string;
50
+ /** A short, safe project label from Claude Code's encoded project dir, or null. */
51
+ export declare function labelFor(pretty: string): string | null;
52
+ export declare function wrap(text: string, width?: number): string[];
53
+ export interface Totals {
54
+ sessions: number;
55
+ /** List-price cost of the requests in the window. */
56
+ actual: number;
57
+ /** The conservative saving on those requests (backtest.estimate). */
58
+ saved: number;
59
+ }
60
+ export interface Biggest {
61
+ label: string | null;
62
+ start: number;
63
+ end: number;
64
+ actual: number;
65
+ saved: number;
66
+ billing: "api" | "subscription";
67
+ }
68
+ export interface ScreenInput {
69
+ /** API-key sessions in the window (null: none). */
70
+ api: Totals | null;
71
+ /** Team / Enterprise subscription sessions in the window (null: none). */
72
+ sub: (Totals & {
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;
80
+ }
81
+ export interface AuditEntry {
82
+ shown: unknown;
83
+ raw: unknown;
84
+ method: string;
85
+ }
86
+ export type Audit = Record<string, AuditEntry>;
87
+ /** -> the screen lines and the audit of every number on them. */
88
+ export declare function buildScreen(inp: ScreenInput): {
89
+ lines: string[];
90
+ audit: Audit;
91
+ };