@mnemom/mnemom 0.17.32 → 0.17.34-next.1

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.
Files changed (88) hide show
  1. package/dist/commands/agent-fuel.d.ts +9 -0
  2. package/dist/commands/agent-fuel.js +36 -2
  3. package/dist/commands/agent-run.d.ts +50 -0
  4. package/dist/commands/agent-run.js +221 -0
  5. package/dist/commands/agent-setup.d.ts +8 -3
  6. package/dist/commands/agent-setup.js +32 -9
  7. package/dist/commands/agent-share-fixture.d.ts +3 -1
  8. package/dist/commands/agent-share-fixture.js +11 -7
  9. package/dist/commands/agent-share-render.d.ts +23 -0
  10. package/dist/commands/agent-share-render.js +72 -0
  11. package/dist/commands/agent-share.d.ts +15 -2
  12. package/dist/commands/agent-share.js +30 -9
  13. package/dist/commands/agent.d.ts +34 -4
  14. package/dist/commands/agent.js +116 -20
  15. package/dist/commands/protection.js +1 -1
  16. package/dist/commands/scan.d.ts +97 -0
  17. package/dist/commands/scan.js +238 -0
  18. package/dist/index.js +55 -0
  19. package/dist/lib/api.d.ts +13 -0
  20. package/dist/lib/api.js +25 -1
  21. package/dist/lib/auth.d.ts +6 -0
  22. package/dist/lib/auth.js +161 -14
  23. package/dist/lib/experimental.d.ts +1 -1
  24. package/dist/lib/experimental.js +13 -0
  25. package/dist/lib/oauth.d.ts +7 -0
  26. package/dist/lib/oauth.js +13 -0
  27. package/dist/lib/runner/bundle.d.ts +129 -0
  28. package/dist/lib/runner/bundle.js +319 -0
  29. package/dist/lib/runner/contract-header.d.ts +46 -0
  30. package/dist/lib/runner/contract-header.js +85 -0
  31. package/dist/lib/runner/executor-client.d.ts +71 -0
  32. package/dist/lib/runner/executor-client.js +97 -0
  33. package/dist/lib/runner/job.d.ts +70 -0
  34. package/dist/lib/runner/job.js +463 -0
  35. package/dist/lib/runner/loop.d.ts +45 -0
  36. package/dist/lib/runner/loop.js +104 -0
  37. package/dist/lib/runner/oidc.d.ts +38 -0
  38. package/dist/lib/runner/oidc.js +89 -0
  39. package/dist/lib/runner/options.d.ts +41 -0
  40. package/dist/lib/runner/options.js +47 -0
  41. package/dist/lib/runner/outcome.d.ts +47 -0
  42. package/dist/lib/runner/outcome.js +89 -0
  43. package/dist/lib/runner/phases.d.ts +49 -0
  44. package/dist/lib/runner/phases.js +99 -0
  45. package/dist/lib/runner/proxy.d.ts +45 -0
  46. package/dist/lib/runner/proxy.js +153 -0
  47. package/dist/lib/runner/settings.d.ts +31 -0
  48. package/dist/lib/runner/settings.js +44 -0
  49. package/dist/lib/scan/caps.d.ts +35 -0
  50. package/dist/lib/scan/caps.js +67 -0
  51. package/dist/lib/scan/credential.d.ts +37 -0
  52. package/dist/lib/scan/credential.js +104 -0
  53. package/dist/lib/scan/failure.d.ts +10 -0
  54. package/dist/lib/scan/failure.js +43 -0
  55. package/dist/lib/scan/pr-source.d.ts +57 -0
  56. package/dist/lib/scan/pr-source.js +130 -0
  57. package/dist/lib/scan/record.d.ts +49 -0
  58. package/dist/lib/scan/record.js +85 -0
  59. package/dist/lib/scan/report-email.d.ts +92 -0
  60. package/dist/lib/scan/report-email.js +136 -0
  61. package/dist/lib/scan/report.d.ts +13 -0
  62. package/dist/lib/scan/report.js +30 -0
  63. package/dist/lib/scan/scan.d.ts +64 -0
  64. package/dist/lib/scan/scan.js +182 -0
  65. package/dist/lib/scan/share.d.ts +38 -0
  66. package/dist/lib/scan/share.js +47 -0
  67. package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
  68. package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
  69. package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
  70. package/dist/lib/scan/spend/anthropic-usage.js +244 -0
  71. package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
  72. package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
  73. package/dist/lib/scan/spend/prices.d.ts +33 -0
  74. package/dist/lib/scan/spend/prices.js +57 -0
  75. package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
  76. package/dist/lib/scan/spend/spend-estimate.js +70 -0
  77. package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
  78. package/dist/lib/scan/spend/spend-tiers.js +42 -0
  79. package/dist/lib/scan/spend/token-rate.d.ts +30 -0
  80. package/dist/lib/scan/spend/token-rate.js +54 -0
  81. package/dist/lib/session-key.d.ts +41 -0
  82. package/dist/lib/session-key.js +124 -0
  83. package/dist/lib/statusline-feed.js +10 -11
  84. package/dist/lib/statusline.d.ts +6 -0
  85. package/dist/lib/statusline.js +19 -9
  86. package/dist/rc-key-401.mjs +75 -0
  87. package/dist/rc-proxy.mjs +290 -30
  88. package/package.json +2 -1
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Share level: what, if anything, may leave the machine. Asked before any output is built for sharing.
3
+ * Default is summary metrics. "none" yields no payload at all.
4
+ */
5
+ import { askSelect } from "../prompt.js";
6
+ export const SHARE_OPTIONS = [
7
+ { level: "summary", label: "Summary metrics only (counts and one estimate line; default)" },
8
+ { level: "full", label: "Full report (summary plus per-repo counts)" },
9
+ { level: "none", label: "Nothing (keep results on this machine)" },
10
+ ];
11
+ /** Asks the user. Anything but a recognised answer (cancel, EOF, noise) is "none": fail closed. */
12
+ export async function promptShareLevel() {
13
+ const answer = await askSelect("What may leave this machine?", SHARE_OPTIONS.map((o) => o.label));
14
+ if (answer === null)
15
+ return "none";
16
+ return SHARE_OPTIONS.find((o) => o.label === answer)?.level ?? "none";
17
+ }
18
+ export function parseShareLevel(v) {
19
+ return v === "summary" || v === "full" || v === "none" ? v : null;
20
+ }
21
+ export function buildSharePayload(level, r, recipients,
22
+ /** undefined: the legacy line; null: no spend line; string: that line. */
23
+ spendLine) {
24
+ if (level === "none")
25
+ return null;
26
+ const base = {
27
+ recipients,
28
+ level,
29
+ period: { start: r.coverage.windowStart, end: r.coverage.windowEnd },
30
+ prs: r.totals.prs,
31
+ reverts: r.totals.reverts,
32
+ ci_green: r.totals.ciGreen,
33
+ examined: r.totals.examined,
34
+ coverage_complete: r.coverage.complete,
35
+ ...((spendLine === undefined ? r.spend.line : spendLine) === null
36
+ ? {}
37
+ : { spend_line: spendLine ?? r.spend.line }),
38
+ };
39
+ if (level === "full")
40
+ base.repos = r.byRepo.map((b) => ({
41
+ repo: b.repo,
42
+ prs: b.prs,
43
+ reverts: b.reverts,
44
+ ci_green: b.ciGreen,
45
+ }));
46
+ return base;
47
+ }
@@ -0,0 +1,6 @@
1
+ import type { AiTool } from "./prices.js";
2
+ /** Upper bound on text scanned per PR, so one huge body cannot dominate a scan. The TAIL is kept: trailers and "generated with" footers sit at the end. */
3
+ export declare const MAX_MARK_TEXT = 20000;
4
+ export declare const AI_TOOLS: AiTool[];
5
+ /** Tools whose mark appears in the text, in a fixed order. Non-string input yields none. */
6
+ export declare function detectAiTools(markText: unknown): AiTool[];
@@ -0,0 +1,18 @@
1
+ /** Upper bound on text scanned per PR, so one huge body cannot dominate a scan. The TAIL is kept: trailers and "generated with" footers sit at the end. */
2
+ export const MAX_MARK_TEXT = 20_000;
3
+ const TRAILER = String.raw `^[ \t>*-]*co-authored-by:[^\n]*?`;
4
+ const GEN = String.raw `generated (?:with|by|using)[^\n]{0,24}?`;
5
+ const MARKS = {
6
+ claude_code: new RegExp(`${TRAILER}(?:\\bclaude\\b|noreply@anthropic\\.com)|${GEN}\\bclaude code\\b`, "im"),
7
+ codex: new RegExp(`${TRAILER}\\bcodex\\b|${GEN}\\bcodex\\b`, "im"),
8
+ copilot: new RegExp(`${TRAILER}\\bcopilot\\b|${GEN}\\bgithub copilot\\b`, "im"),
9
+ gemini: new RegExp(`${TRAILER}\\bgemini\\b|${GEN}\\bgemini(?: cli)?\\b`, "im"),
10
+ };
11
+ export const AI_TOOLS = Object.keys(MARKS);
12
+ /** Tools whose mark appears in the text, in a fixed order. Non-string input yields none. */
13
+ export function detectAiTools(markText) {
14
+ if (typeof markText !== "string" || markText.length === 0)
15
+ return [];
16
+ const text = markText.length > MAX_MARK_TEXT ? markText.slice(-MAX_MARK_TEXT) : markText;
17
+ return AI_TOOLS.filter((t) => MARKS[t].test(text));
18
+ }
@@ -0,0 +1,57 @@
1
+ export declare const ANTHROPIC_ADMIN_ORIGIN = "https://api.anthropic.com";
2
+ export declare const ADMIN_KEY_ENV = "ANTHROPIC_ADMIN_KEY";
3
+ export declare const DEFAULT_MAX_PAGES = 20;
4
+ /** One shared deadline for a request including its retries (not N x timeout). */
5
+ export declare const DEFAULT_TIMEOUT_MS = 15000;
6
+ export declare const DEFAULT_MAX_RETRIES = 2;
7
+ /** Shape check only; the key is never echoed, even on a mismatch. */
8
+ export declare const isAdminKeyShape: (k: unknown) => k is string;
9
+ export type UsageFailure = "key_invalid" | "key_refused" | "rate_limited" | "timeout" | "network" | "upstream" | "malformed" | "too_large";
10
+ /** Fixed sentence for a failure reason. */
11
+ export declare const usageFailureText: (r: UsageFailure) => string;
12
+ /** Carries only a reason tag. Its message is the fixed sentence: nothing upstream can reach it. */
13
+ export declare class AnthropicUsageError extends Error {
14
+ readonly reason: UsageFailure;
15
+ constructor(reason: UsageFailure);
16
+ }
17
+ export type ContextWindow = "0-200k" | "200k-1M";
18
+ export interface UsageRow {
19
+ tokens: number;
20
+ contextWindow: ContextWindow | null;
21
+ }
22
+ export interface Page<R> {
23
+ rows: R[];
24
+ truncated: boolean;
25
+ pages: number;
26
+ }
27
+ export interface CostRow {
28
+ /** Cents, as Anthropic returns it (a decimal string). */
29
+ cents: number;
30
+ currency: string;
31
+ }
32
+ export interface AnthropicUsageDeps {
33
+ fetchImpl?: typeof fetch;
34
+ sleep?: (ms: number) => Promise<void>;
35
+ maxPages?: number;
36
+ timeoutMs?: number;
37
+ maxRetries?: number;
38
+ }
39
+ export declare function parseUsageRow(raw: unknown): UsageRow;
40
+ export declare function parseCostRow(raw: unknown): CostRow;
41
+ /** Buckets are UTC-aligned: floor so a mid-day start reads the whole bucket it falls in. */
42
+ export declare function alignStartingAt(startingAt: string): string;
43
+ export interface MeasuredUsage {
44
+ /** Whole-org cost over the period, in USD. */
45
+ usd: number;
46
+ /** Fraction in [0, 1] of tokens from requests in the 200k-1M context window; null with no tokens. */
47
+ over200kShare: number | null;
48
+ }
49
+ /**
50
+ * The same two reads mnemom-api made: usage grouped by context_window (the exact >200k share) and cost
51
+ * (total spend). The key is shape-checked before any call so a regular API key never leaves the machine.
52
+ * Throws AnthropicUsageError only. A walk stopped by the page cap is an error, never a short total.
53
+ */
54
+ export declare function fetchMeasuredUsage(key: string | undefined, period: {
55
+ start: string;
56
+ end: string;
57
+ }, deps?: AnthropicUsageDeps): Promise<MeasuredUsage>;
@@ -0,0 +1,244 @@
1
+ // MNE-9298 Tier 1: the CTO's own Anthropic Admin key, used LOCALLY, to read measured usage. Port of
2
+ // mnemom-api src/org-scan/usage/usage-client.ts (two read-only GETs: usage_report/messages grouped by
3
+ // context_window, and cost_report) with the server-only parts (egress allowlist, WIF) dropped.
4
+ //
5
+ // The key is held in memory for the length of the two reads. It is never stored, logged, written to disk,
6
+ // echoed, put in argv, a URL or an error message, and is sent only to api.anthropic.com (a constant, not
7
+ // configurable) in the x-api-key header. Every failure surfaces as a fixed sentence: the thrown value, the
8
+ // response body and the key never reach the terminal.
9
+ export const ANTHROPIC_ADMIN_ORIGIN = "https://api.anthropic.com";
10
+ export const ADMIN_KEY_ENV = "ANTHROPIC_ADMIN_KEY";
11
+ const ANTHROPIC_VERSION = "2023-06-01";
12
+ const USAGE_PATH = "/v1/organizations/usage_report/messages";
13
+ const COST_PATH = "/v1/organizations/cost_report";
14
+ export const DEFAULT_MAX_PAGES = 20;
15
+ /** One shared deadline for a request including its retries (not N x timeout). */
16
+ export const DEFAULT_TIMEOUT_MS = 15_000;
17
+ export const DEFAULT_MAX_RETRIES = 2;
18
+ const MAX_RETRY_WAIT_MS = 5_000;
19
+ const ADMIN_KEY_RE = /^sk-ant-admin[A-Za-z0-9_-]{8,}$/;
20
+ /** Shape check only; the key is never echoed, even on a mismatch. */
21
+ export const isAdminKeyShape = (k) => typeof k === "string" && ADMIN_KEY_RE.test(k.trim());
22
+ const FAILURE_TEXT = {
23
+ key_invalid: "The Anthropic admin key is not an Admin API key (it should start with sk-ant-admin).",
24
+ key_refused: "Anthropic refused the admin key (401/403).",
25
+ rate_limited: "Anthropic rate limit reached.",
26
+ timeout: "Anthropic did not answer in time.",
27
+ network: "Anthropic was unreachable.",
28
+ upstream: "Anthropic usage could not be read.",
29
+ malformed: "Anthropic returned usage in a shape this CLI does not recognise.",
30
+ too_large: "Anthropic usage for this period is too large to read completely.",
31
+ };
32
+ /** Fixed sentence for a failure reason. */
33
+ export const usageFailureText = (r) => FAILURE_TEXT[r];
34
+ /** Carries only a reason tag. Its message is the fixed sentence: nothing upstream can reach it. */
35
+ export class AnthropicUsageError extends Error {
36
+ reason;
37
+ constructor(reason) {
38
+ super(FAILURE_TEXT[reason]);
39
+ this.reason = reason;
40
+ this.name = "AnthropicUsageError";
41
+ }
42
+ }
43
+ const bad = () => {
44
+ throw new AnthropicUsageError("malformed");
45
+ };
46
+ const rec = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : bad();
47
+ function reqNum(r, k) {
48
+ const v = r[k];
49
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : bad();
50
+ }
51
+ /** Optional counter inside an optional object: absent is 0, present-but-wrong is an error. */
52
+ function optNum(o, k) {
53
+ if (o === undefined || o === null)
54
+ return 0;
55
+ if (typeof o !== "object")
56
+ return bad();
57
+ const v = o[k];
58
+ if (v === undefined || v === null)
59
+ return 0;
60
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : bad();
61
+ }
62
+ export function parseUsageRow(raw) {
63
+ const r = rec(raw);
64
+ const cw = r.context_window;
65
+ if (cw !== null && cw !== undefined && cw !== "0-200k" && cw !== "200k-1M")
66
+ bad();
67
+ return {
68
+ tokens: reqNum(r, "uncached_input_tokens") +
69
+ reqNum(r, "cache_read_input_tokens") +
70
+ reqNum(r, "output_tokens") +
71
+ optNum(r.cache_creation, "ephemeral_1h_input_tokens") +
72
+ optNum(r.cache_creation, "ephemeral_5m_input_tokens"),
73
+ contextWindow: (cw ?? null),
74
+ };
75
+ }
76
+ const CENT = /^-?\d+(\.\d+)?$/;
77
+ export function parseCostRow(raw) {
78
+ const r = rec(raw);
79
+ if (typeof r.amount !== "string" || !CENT.test(r.amount))
80
+ bad();
81
+ if (typeof r.currency !== "string" || r.currency === "")
82
+ bad();
83
+ return { cents: Number(r.amount), currency: r.currency };
84
+ }
85
+ const DAY_MS = 86_400_000;
86
+ /** Buckets are UTC-aligned: floor so a mid-day start reads the whole bucket it falls in. */
87
+ export function alignStartingAt(startingAt) {
88
+ const t = Date.parse(startingAt);
89
+ if (!Number.isFinite(t))
90
+ return startingAt;
91
+ return new Date(Math.floor(t / DAY_MS) * DAY_MS).toISOString().replace(".000Z", "Z");
92
+ }
93
+ const backoff = (attempt) => Math.min(250 * 2 ** (attempt - 1), MAX_RETRY_WAIT_MS);
94
+ function retryWait(res, attempt) {
95
+ const ra = Number(res.headers.get("retry-after"));
96
+ return Number.isFinite(ra) && ra > 0 ? ra * 1000 : backoff(attempt);
97
+ }
98
+ class Reader {
99
+ key;
100
+ fetchImpl;
101
+ sleep;
102
+ maxPages;
103
+ timeoutMs;
104
+ maxRetries;
105
+ constructor(key, deps) {
106
+ this.key = key;
107
+ this.fetchImpl = deps.fetchImpl ?? ((...a) => fetch(...a));
108
+ this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
109
+ this.maxPages = deps.maxPages ?? DEFAULT_MAX_PAGES;
110
+ this.timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
111
+ this.maxRetries = deps.maxRetries ?? DEFAULT_MAX_RETRIES;
112
+ }
113
+ async walk(path, base, parse, callMaxPages) {
114
+ const ceiling = typeof callMaxPages === "number" && Number.isFinite(callMaxPages) && callMaxPages >= 1
115
+ ? Math.min(this.maxPages, Math.floor(callMaxPages))
116
+ : this.maxPages;
117
+ const rows = [];
118
+ let page = null;
119
+ let pages = 0;
120
+ for (;;) {
121
+ const params = new URLSearchParams(base);
122
+ if (page)
123
+ params.set("page", page);
124
+ const body = await this.request(`${ANTHROPIC_ADMIN_ORIGIN}${path}?${params.toString()}`);
125
+ pages += 1;
126
+ if (!Array.isArray(body.data))
127
+ bad();
128
+ for (const raw of body.data) {
129
+ const b = rec(raw);
130
+ if (typeof b.starting_at !== "string" || typeof b.ending_at !== "string")
131
+ bad();
132
+ if (!Array.isArray(b.results))
133
+ bad();
134
+ for (const r of b.results)
135
+ rows.push(parse(r));
136
+ }
137
+ const more = body.has_more;
138
+ if (more !== undefined && more !== null && typeof more !== "boolean")
139
+ bad();
140
+ if (more !== true)
141
+ return { rows, truncated: false, pages };
142
+ if (typeof body.next_page !== "string" || body.next_page === "")
143
+ bad();
144
+ if (pages >= ceiling)
145
+ return { rows, truncated: true, pages };
146
+ page = body.next_page;
147
+ }
148
+ }
149
+ async request(url) {
150
+ const deadline = AbortSignal.timeout(this.timeoutMs);
151
+ for (let attempt = 0;;) {
152
+ let res;
153
+ try {
154
+ res = await this.fetchImpl(url, {
155
+ method: "GET",
156
+ headers: {
157
+ "x-api-key": this.key,
158
+ "anthropic-version": ANTHROPIC_VERSION,
159
+ accept: "application/json",
160
+ },
161
+ // A redirect would carry the x-api-key header to wherever it points: refuse to follow one.
162
+ redirect: "error",
163
+ signal: deadline,
164
+ });
165
+ }
166
+ catch {
167
+ // Never surface the thrown value: it can carry the request.
168
+ if (deadline.aborted)
169
+ throw new AnthropicUsageError("timeout");
170
+ if (attempt++ < this.maxRetries) {
171
+ await this.sleep(backoff(attempt));
172
+ continue;
173
+ }
174
+ throw new AnthropicUsageError("network");
175
+ }
176
+ if (res.ok) {
177
+ let json;
178
+ try {
179
+ json = await res.json();
180
+ }
181
+ catch {
182
+ throw new AnthropicUsageError("upstream");
183
+ }
184
+ return rec(json);
185
+ }
186
+ await res.body?.cancel().catch(() => { });
187
+ if (res.status === 401 || res.status === 403)
188
+ throw new AnthropicUsageError("key_refused");
189
+ const retryable = res.status === 429 || res.status >= 500;
190
+ const wait = retryable ? retryWait(res, attempt + 1) : 0;
191
+ if (retryable &&
192
+ !deadline.aborted &&
193
+ wait <= MAX_RETRY_WAIT_MS &&
194
+ attempt++ < this.maxRetries) {
195
+ await this.sleep(wait);
196
+ continue;
197
+ }
198
+ throw new AnthropicUsageError(res.status === 429 ? "rate_limited" : "upstream");
199
+ }
200
+ }
201
+ }
202
+ const validDay = (d) => /^\d{4}-\d{2}-\d{2}$/.test(d) && Number.isFinite(Date.parse(`${d}T00:00:00Z`));
203
+ /**
204
+ * The same two reads mnemom-api made: usage grouped by context_window (the exact >200k share) and cost
205
+ * (total spend). The key is shape-checked before any call so a regular API key never leaves the machine.
206
+ * Throws AnthropicUsageError only. A walk stopped by the page cap is an error, never a short total.
207
+ */
208
+ export async function fetchMeasuredUsage(key, period, deps = {}) {
209
+ const k = key?.trim();
210
+ if (!k || !isAdminKeyShape(k))
211
+ throw new AnthropicUsageError("key_invalid");
212
+ if (!validDay(period.start) || !validDay(period.end))
213
+ throw new AnthropicUsageError("malformed");
214
+ const startingAt = alignStartingAt(`${period.start}T00:00:00Z`);
215
+ const endingAt = new Date(Date.parse(`${period.end}T00:00:00Z`) + DAY_MS).toISOString();
216
+ const reader = new Reader(k, deps);
217
+ const up = new URLSearchParams({
218
+ starting_at: startingAt,
219
+ ending_at: endingAt,
220
+ bucket_width: "1d",
221
+ });
222
+ up.append("group_by[]", "context_window");
223
+ const usage = await reader.walk(USAGE_PATH, up, parseUsageRow);
224
+ if (usage.truncated)
225
+ throw new AnthropicUsageError("too_large");
226
+ const cp = new URLSearchParams({ starting_at: startingAt, ending_at: endingAt });
227
+ const cost = await reader.walk(COST_PATH, cp, parseCostRow);
228
+ if (cost.truncated)
229
+ throw new AnthropicUsageError("too_large");
230
+ let total = 0;
231
+ let over = 0;
232
+ for (const r of usage.rows) {
233
+ total += r.tokens;
234
+ if (r.contextWindow === "200k-1M")
235
+ over += r.tokens;
236
+ }
237
+ let cents = 0;
238
+ for (const r of cost.rows) {
239
+ if (r.currency !== "USD")
240
+ throw new AnthropicUsageError("malformed");
241
+ cents += r.cents;
242
+ }
243
+ return { usd: Math.round(cents) / 100, over200kShare: total > 0 ? over / total : null };
244
+ }
@@ -0,0 +1,56 @@
1
+ declare const _default: {
2
+ readonly schema_version: 1;
3
+ readonly as_of: "2026-10-28T00:00:00Z";
4
+ readonly source: "ADW 'Run cost' PR comments (cost_report.py) on merged fleet PRs (org-wide GitHub search for run-cost comments, plus shipped PRs from the eval sets); cost_usd sums distinct runs and is a floor";
5
+ readonly candidates: 236;
6
+ readonly rows: readonly [{
7
+ readonly repo: "aap";
8
+ readonly pr: 100;
9
+ readonly additions: 117;
10
+ readonly deletions: 68;
11
+ readonly cost_usd: 0.4;
12
+ readonly runs: 1;
13
+ readonly models: readonly ["sonnet"];
14
+ }, {
15
+ readonly repo: "mnemom-api";
16
+ readonly pr: 1816;
17
+ readonly additions: 239;
18
+ readonly deletions: 0;
19
+ readonly cost_usd: 0.55;
20
+ readonly runs: 1;
21
+ readonly models: readonly ["haiku", "sonnet"];
22
+ }, {
23
+ readonly repo: "mnemom-api";
24
+ readonly pr: 3639;
25
+ readonly additions: 2260;
26
+ readonly deletions: 0;
27
+ readonly cost_usd: 5.42;
28
+ readonly runs: 1;
29
+ readonly models: readonly ["sonnet"];
30
+ }, {
31
+ readonly repo: "mnemom-research-grade";
32
+ readonly pr: 112;
33
+ readonly additions: 73;
34
+ readonly deletions: 10;
35
+ readonly cost_usd: 0.35;
36
+ readonly runs: 1;
37
+ readonly models: readonly ["haiku", "sonnet"];
38
+ }, {
39
+ readonly repo: "mnemom-website";
40
+ readonly pr: 1668;
41
+ readonly additions: 78;
42
+ readonly deletions: 119;
43
+ readonly cost_usd: 0.23;
44
+ readonly runs: 1;
45
+ readonly models: readonly ["sonnet"];
46
+ }, {
47
+ readonly repo: "mnemom-website";
48
+ readonly pr: 1669;
49
+ readonly additions: 126;
50
+ readonly deletions: 22;
51
+ readonly cost_usd: 0.28;
52
+ readonly runs: 1;
53
+ readonly models: readonly ["sonnet"];
54
+ }];
55
+ };
56
+ export default _default;
@@ -0,0 +1,64 @@
1
+ // Cost corpus (port of mnemom-api src/org-scan/spend/cost-corpus.v1.json, as_of 2026-10-28T00:00:00Z). A TS module, not a
2
+ // JSON asset, so the build needs no copy-assets entry.
3
+ export default {
4
+ schema_version: 1,
5
+ as_of: "2026-10-28T00:00:00Z",
6
+ source: "ADW 'Run cost' PR comments (cost_report.py) on merged fleet PRs (org-wide GitHub search for run-cost comments, plus shipped PRs from the eval sets); cost_usd sums distinct runs and is a floor",
7
+ candidates: 236,
8
+ rows: [
9
+ {
10
+ repo: "aap",
11
+ pr: 100,
12
+ additions: 117,
13
+ deletions: 68,
14
+ cost_usd: 0.4,
15
+ runs: 1,
16
+ models: ["sonnet"],
17
+ },
18
+ {
19
+ repo: "mnemom-api",
20
+ pr: 1816,
21
+ additions: 239,
22
+ deletions: 0,
23
+ cost_usd: 0.55,
24
+ runs: 1,
25
+ models: ["haiku", "sonnet"],
26
+ },
27
+ {
28
+ repo: "mnemom-api",
29
+ pr: 3639,
30
+ additions: 2260,
31
+ deletions: 0,
32
+ cost_usd: 5.42,
33
+ runs: 1,
34
+ models: ["sonnet"],
35
+ },
36
+ {
37
+ repo: "mnemom-research-grade",
38
+ pr: 112,
39
+ additions: 73,
40
+ deletions: 10,
41
+ cost_usd: 0.35,
42
+ runs: 1,
43
+ models: ["haiku", "sonnet"],
44
+ },
45
+ {
46
+ repo: "mnemom-website",
47
+ pr: 1668,
48
+ additions: 78,
49
+ deletions: 119,
50
+ cost_usd: 0.23,
51
+ runs: 1,
52
+ models: ["sonnet"],
53
+ },
54
+ {
55
+ repo: "mnemom-website",
56
+ pr: 1669,
57
+ additions: 126,
58
+ deletions: 22,
59
+ cost_usd: 0.28,
60
+ runs: 1,
61
+ models: ["sonnet"],
62
+ },
63
+ ],
64
+ };
@@ -0,0 +1,33 @@
1
+ export type AiTool = "claude_code" | "codex" | "copilot" | "gemini";
2
+ export interface TokenPrice {
3
+ /** USD per 1M input tokens. */
4
+ inputPerMTok: number;
5
+ /** USD per 1M output tokens. */
6
+ outputPerMTok: number;
7
+ model: string;
8
+ }
9
+ export interface PriceBand {
10
+ low: TokenPrice;
11
+ high: TokenPrice;
12
+ /** Page the prices were read from. */
13
+ source: string;
14
+ }
15
+ export declare const PRICES_AS_OF = "2026-10-28";
16
+ export declare const TOOL_PRICES: Record<Exclude<AiTool, "copilot">, PriceBand>;
17
+ /**
18
+ * GitHub Copilot bills per premium request, not per token
19
+ * (https://docs.github.com/en/copilot/concepts/billing/copilot-requests),
20
+ * so it has no token price of its own. Its agent routes to the other vendors' models, so its band is
21
+ * the union of the other three: cheapest low to dearest high. An assumption, stated here once.
22
+ */
23
+ export declare function copilotBand(): PriceBand;
24
+ /**
25
+ * Share of billed tokens that are input (the rest output). Agent loops re-read large context and
26
+ * write little, so input dominates. An ASSUMPTION used both to turn our known run cost into
27
+ * tokens-per-line and to price those tokens for each tool; it cancels to first order when the
28
+ * tool's input/output price ratio matches ours, and is the main source of the range's width.
29
+ */
30
+ export declare const INPUT_SHARE = 0.9;
31
+ export declare function bandFor(tool: AiTool): PriceBand;
32
+ /** USD per token at the assumed input/output mix. */
33
+ export declare function blendedUsdPerToken(p: TokenPrice): number;
@@ -0,0 +1,57 @@
1
+ // MNE-9268: public list prices for the AI coding tools the Tier-3 spend estimator can name.
2
+ // Constants, not magic numbers: every row carries its source page and the date it was read.
3
+ // USD per million tokens (input / output), standard (non-batch) list price. A band is the cheapest
4
+ // and the dearest model a tool's coding agent plausibly runs on, so the estimate is a RANGE.
5
+ // Re-read the pages and bump PRICES_AS_OF when they move; a stale price widens nothing silently,
6
+ // it just shifts the range, which is why the date ships with the estimate's method text.
7
+ export const PRICES_AS_OF = "2026-10-28";
8
+ export const TOOL_PRICES = {
9
+ claude_code: {
10
+ source: "https://platform.claude.com/docs/en/about-claude/pricing",
11
+ low: { model: "Claude Sonnet 5.5", inputPerMTok: 2, outputPerMTok: 10 },
12
+ high: { model: "Claude Opus 5", inputPerMTok: 5, outputPerMTok: 25 },
13
+ },
14
+ codex: {
15
+ source: "https://developers.openai.com/api/docs/pricing",
16
+ low: { model: "gpt-5.3-codex (standard)", inputPerMTok: 1.75, outputPerMTok: 14 },
17
+ high: { model: "gpt-5.3-codex (fast)", inputPerMTok: 3.5, outputPerMTok: 28 },
18
+ },
19
+ gemini: {
20
+ source: "https://ai.google.dev/gemini-api/docs/pricing",
21
+ // Gemini 3.8 Flash is $0.75/$3.75 through 2026-12-31 and $1.50/$7.50 from 2027-01-01; the
22
+ // post-promotion price is used so the lower bound does not expire in two months.
23
+ low: { model: "Gemini 3.8 Flash (from 2027-01-01)", inputPerMTok: 1.5, outputPerMTok: 7.5 },
24
+ high: { model: "Gemini 3.1 Pro (prompts up to 200k)", inputPerMTok: 2, outputPerMTok: 12 },
25
+ },
26
+ };
27
+ /**
28
+ * GitHub Copilot bills per premium request, not per token
29
+ * (https://docs.github.com/en/copilot/concepts/billing/copilot-requests),
30
+ * so it has no token price of its own. Its agent routes to the other vendors' models, so its band is
31
+ * the union of the other three: cheapest low to dearest high. An assumption, stated here once.
32
+ */
33
+ export function copilotBand() {
34
+ const bands = Object.values(TOOL_PRICES);
35
+ const blend = (p) => p.inputPerMTok * INPUT_SHARE + p.outputPerMTok * (1 - INPUT_SHARE);
36
+ const low = bands.map((b) => b.low).sort((a, b) => blend(a) - blend(b))[0];
37
+ const high = bands.map((b) => b.high).sort((a, b) => blend(b) - blend(a))[0];
38
+ return {
39
+ low,
40
+ high,
41
+ source: "union of the claude_code, codex and gemini bands (per-request billing)",
42
+ };
43
+ }
44
+ /**
45
+ * Share of billed tokens that are input (the rest output). Agent loops re-read large context and
46
+ * write little, so input dominates. An ASSUMPTION used both to turn our known run cost into
47
+ * tokens-per-line and to price those tokens for each tool; it cancels to first order when the
48
+ * tool's input/output price ratio matches ours, and is the main source of the range's width.
49
+ */
50
+ export const INPUT_SHARE = 0.9;
51
+ export function bandFor(tool) {
52
+ return tool === "copilot" ? copilotBand() : TOOL_PRICES[tool];
53
+ }
54
+ /** USD per token at the assumed input/output mix. */
55
+ export function blendedUsdPerToken(p) {
56
+ return (p.inputPerMTok * INPUT_SHARE + p.outputPerMTok * (1 - INPUT_SHARE)) / 1_000_000;
57
+ }
@@ -0,0 +1,30 @@
1
+ import { type AiTool } from "./prices.js";
2
+ import { type TokenRate } from "./token-rate.js";
3
+ /** AI-assisted PRs needed before a range is worth printing. */
4
+ export declare const MIN_AI_PRS = 10;
5
+ /** Per-PR input. `markText` is commit messages / PR body, read for tool marks and dropped. */
6
+ export interface SpendPrInput {
7
+ additions: number;
8
+ deletions: number;
9
+ markText: string;
10
+ /** Tools already detected inside the PR source (so the body never leaves it). Wins over markText. */
11
+ tools?: AiTool[];
12
+ }
13
+ export type NoSpendReason = "too_few_ai_prs" | "no_rate" | "invalid_input";
14
+ export type SpendEstimate = {
15
+ kind: "range";
16
+ lowUsd: number;
17
+ highUsd: number;
18
+ aiPrs: number;
19
+ scannedPrs: number;
20
+ byTool: Partial<Record<AiTool, number>>;
21
+ /** One line, labelled estimate, passes claim-lint. */
22
+ line: string;
23
+ } | {
24
+ kind: "none";
25
+ reason: NoSpendReason;
26
+ aiPrs: number;
27
+ scannedPrs: number;
28
+ line: string;
29
+ };
30
+ export declare function estimateSpend(prs: unknown, rate?: TokenRate | null): SpendEstimate;