@kunal_dutt/token-lens 0.0.0-stage → 0.2.17

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 (40) hide show
  1. package/GATEWAY.md +491 -0
  2. package/LICENSE +21 -0
  3. package/PRIVACY.md +36 -0
  4. package/README.md +80 -2
  5. package/agent-cost-check.yml +49 -0
  6. package/check-agent.mjs +3298 -0
  7. package/cli.mjs +22 -0
  8. package/gateway.mjs +3365 -0
  9. package/package.json +45 -4
  10. package/sdk.mjs +207719 -0
  11. package/server.mjs +3392 -0
  12. package/token-lens.mcpb +0 -0
  13. package/types/src/core/advice.d.ts +27 -0
  14. package/types/src/core/budget.d.ts +28 -0
  15. package/types/src/core/compress.d.ts +29 -0
  16. package/types/src/core/cost.d.ts +32 -0
  17. package/types/src/core/guard.d.ts +20 -0
  18. package/types/src/core/hotspots.d.ts +124 -0
  19. package/types/src/core/index.d.ts +74 -0
  20. package/types/src/core/media.d.ts +29 -0
  21. package/types/src/core/models.d.ts +81 -0
  22. package/types/src/core/plans.d.ts +88 -0
  23. package/types/src/core/redos.d.ts +32 -0
  24. package/types/src/core/review.d.ts +46 -0
  25. package/types/src/core/rules.d.ts +47 -0
  26. package/types/src/core/tasks.d.ts +44 -0
  27. package/types/src/core/tokens.d.ts +19 -0
  28. package/types/src/core/verify.d.ts +19 -0
  29. package/types/src/gateway/config.d.ts +217 -0
  30. package/types/src/gateway/guard.d.ts +56 -0
  31. package/types/src/gateway/images.d.ts +40 -0
  32. package/types/src/gateway/metrics.d.ts +287 -0
  33. package/types/src/gateway/optimize.d.ts +102 -0
  34. package/types/src/gateway/protocol.d.ts +68 -0
  35. package/types/src/gateway/runs.d.ts +182 -0
  36. package/types/src/gateway/shared.d.ts +57 -0
  37. package/types/src/gateway/signals.d.ts +38 -0
  38. package/types/src/gateway/storage.d.ts +88 -0
  39. package/types/src/gateway/verify.d.ts +51 -0
  40. package/types/src/sdk/index.d.ts +228 -0
Binary file
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Tips that save tokens beyond rewriting the prompt: picking the right model,
3
+ * capping the answer, pasting less, and handing off long chats. Each tip
4
+ * depends on the model, the task and the conversation, so they change as the
5
+ * user switches model or types a different kind of prompt.
6
+ */
7
+ import type { ContextUsage } from "./budget";
8
+ import type { ModelProfile } from "./models";
9
+ import type { Task, TaskSignals } from "./tasks";
10
+ export interface Tip {
11
+ id: string;
12
+ text: string;
13
+ /** What the tip saves: tokens sent, tokens generated, or hidden reasoning. */
14
+ saves: "input" | "output" | "reasoning";
15
+ /** Text the UI can append to the prompt to apply the tip in one click. */
16
+ append?: string;
17
+ }
18
+ export interface AdviceInput {
19
+ profile: ModelProfile;
20
+ task: Task;
21
+ signals: TaskSignals;
22
+ promptTokens: number;
23
+ usage: ContextUsage;
24
+ /** The prompt, to check for things like XML tags. */
25
+ text: string;
26
+ }
27
+ export declare function advise(input: AdviceInput): Tip[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Conversation budget: how full the model's context is, how much each new
3
+ * message re-reads, and when to hand off to a fresh chat.
4
+ */
5
+ import type { ModelProfile } from "./models";
6
+ import type { Level } from "./compress";
7
+ import type { Task } from "./tasks";
8
+ export type Zone = "ok" | "warm" | "hot" | "full";
9
+ export interface ContextUsage {
10
+ conversationTokens: number;
11
+ promptTokens: number;
12
+ window: number;
13
+ /** (conversation + prompt) / window, 0..1+ */
14
+ ratio: number;
15
+ zone: Zone;
16
+ /** Long enough that every new message re-reads a lot, whatever the window. */
17
+ longChat: boolean;
18
+ }
19
+ /** Past this many tokens a chat is worth handing off even on a huge context window. */
20
+ export declare const LONG_CHAT_TOKENS = 40000;
21
+ export declare function contextUsage(conversationTokens: number, promptTokens: number, profile: ModelProfile, windowOverride?: number): ContextUsage;
22
+ /** "auto" tightens compression as the context fills; an explicit level is respected. */
23
+ export declare function chooseLevel(setting: Level | "auto", usage: ContextUsage): Level;
24
+ /**
25
+ * A prompt that asks the current model to compress the chat into a brief the
26
+ * user can paste into a new chat, so the next turns stop re-reading history.
27
+ */
28
+ export declare function handoffPrompt(task: Task): string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Rule-based prompt compression that adapts to the model and the task.
3
+ *
4
+ * - Only the user's instructions are rewritten. Code, inline code, URLs,
5
+ * XML-tagged blocks, quotes, and (for most tasks) material pasted below the
6
+ * instruction are left exactly as written.
7
+ * - Reasoning models get chain-of-thought boilerplate removed; they think anyway.
8
+ * - Debugging prompts get pasted logs trimmed: repeated lines and library
9
+ * stack frames are collapsed.
10
+ * - The result is only offered when it is shorter in the model's own tokens.
11
+ */
12
+ import type { ModelProfile } from "./models";
13
+ import { type Task } from "./tasks";
14
+ export type Level = "light" | "balanced" | "aggressive";
15
+ export declare const LEVELS: Level[];
16
+ export interface CompressOptions {
17
+ profile: ModelProfile;
18
+ task: Task;
19
+ level: Level;
20
+ }
21
+ export interface CompressResult {
22
+ text: string;
23
+ /** Names of the rules that changed something, in first-applied order. */
24
+ rules: string[];
25
+ originalTokens: number;
26
+ tokens: number;
27
+ saved: number;
28
+ }
29
+ export declare function compress(text: string, opts: CompressOptions): CompressResult;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Cost: what tokens cost, and what a saving is worth, priced for the model in
3
+ * use. Prices come from the model profile (API list prices by default, or the
4
+ * user's own), so the same saving is worth more on a pricier model.
5
+ */
6
+ import type { ModelPricing, ModelProfile } from "./models";
7
+ export type TokenKind = "input" | "output" | "cached";
8
+ export interface Savings {
9
+ /** Tokens saved per call. */
10
+ tokens: number;
11
+ /** USD saved per call; null when the model has no price. */
12
+ usd: number | null;
13
+ /** USD saved over 1,000 calls; null when the model has no price. */
14
+ usdPer1k: number | null;
15
+ }
16
+ /** USD per token of this kind, or null when the model has no price. */
17
+ export declare function perToken(profile: ModelProfile, kind?: TokenKind): number | null;
18
+ /** USD for `tokens` of this kind, or null when the model has no price. */
19
+ export declare function costOf(tokens: number, profile: ModelProfile, kind?: TokenKind): number | null;
20
+ /** What `tokens` fewer input tokens per call are worth. */
21
+ export declare function savingsOf(tokens: number, profile: ModelProfile, kind?: TokenKind): Savings;
22
+ /** What moving `tokens` from full-price input to cached input saves per call. */
23
+ export declare function cachingSavings(tokens: number, profile: ModelProfile): Savings;
24
+ /**
25
+ * Dollars, with enough digits that small per-prompt amounts don't show as $0.00:
26
+ * $12.40, $0.84, $0.0031, $0.000042.
27
+ */
28
+ export declare function formatUsd(usd: number | null | undefined): string;
29
+ /** "$4 in / $20 out per 1M tokens" */
30
+ export declare function describePricing(p: ModelPricing | undefined): string;
31
+ /** One line for a saving: "Saves 1,240 tokens ($0.0050) per call, $4.96 per 1,000 calls". */
32
+ export declare function describeSavings(s: Savings, unit?: string): string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Meaning guard: a rewrite may drop words, never facts or constraints.
3
+ * Every number, URL, inline code span, quoted string, negation and hard
4
+ * constraint word in the original must still be in the rewrite, at least as
5
+ * often. A rewrite that fails is discarded in favour of the original wording.
6
+ */
7
+ export interface GuardResult {
8
+ ok: boolean;
9
+ /** Anchors the rewrite lost, e.g. "negation:not". */
10
+ lost: string[];
11
+ }
12
+ /**
13
+ * `count` (default): every anchor as often as before. `presence`: every anchor
14
+ * at least once, for comparing texts where exact repeated sentences were removed.
15
+ */
16
+ export declare function checkMeaning(original: string, rewritten: string, mode?: "count" | "presence"): GuardResult;
17
+ /** Distinct anchors of each kind in the original and how many the rewrite kept, e.g. { number: [3, 3] }. */
18
+ export declare function anchorSummary(original: string, rewritten: string): Record<string, [number, number]>;
19
+ /** Content words of the original that are missing from the rewrite (beyond allowed filler). */
20
+ export declare function lostWords(original: string, rewritten: string): string[];
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Hotspots: where the tokens go in a prompt, an agent (system prompt, tools,
3
+ * examples, conversation) or a chat, where they are growing, and the fix for
4
+ * each spot.
5
+ *
6
+ * Every fix is classed by risk to the output:
7
+ * safe keeps the meaning (passes the meaning guard) or doesn't touch what
8
+ * the model reads at all (e.g. prompt caching); applied automatically.
9
+ * review could change the answer (dropping history, fewer examples, a
10
+ * smaller model, a length cap); listed for the user to opt into.
11
+ */
12
+ import { type Level } from "./compress";
13
+ import { type Savings } from "./cost";
14
+ import { type ModelProfile } from "./models";
15
+ import { type Verification } from "./verify";
16
+ export interface ToolDef {
17
+ name: string;
18
+ description?: string;
19
+ /** JSON schema of the parameters (OpenAI `parameters` / Anthropic `input_schema`). */
20
+ parameters?: unknown;
21
+ }
22
+ export interface Message {
23
+ role: "user" | "assistant" | "tool" | "system";
24
+ content: string;
25
+ /** Tool calls the assistant made in this message, by tool name. */
26
+ toolCalls?: string[];
27
+ /** Token count if the caller already has it (skips recounting). */
28
+ tokens?: number;
29
+ }
30
+ export interface AgentSpec {
31
+ system?: string;
32
+ tools?: ToolDef[];
33
+ examples?: string[];
34
+ messages?: Message[];
35
+ /** The next user message about to be sent. */
36
+ prompt?: string;
37
+ }
38
+ export type Area = "system" | "tools" | "examples" | "history" | "prompt" | "output";
39
+ export type Risk = "safe" | "review";
40
+ export interface Finding {
41
+ id: string;
42
+ area: Area;
43
+ /** What is happening, in one sentence. */
44
+ issue: string;
45
+ /** What to do about it. */
46
+ fix: string;
47
+ risk: Risk;
48
+ /** Tokens saved per call if fixed (estimate). */
49
+ saves: number;
50
+ /** USD saved per call at the model's price (caching fixes count the cached-price discount); null without a price. */
51
+ usd: number | null;
52
+ /** True when `optimizeAgent` applied it. */
53
+ applied?: boolean;
54
+ }
55
+ export interface Breakdown {
56
+ area: Area;
57
+ label: string;
58
+ tokens: number;
59
+ }
60
+ export interface HotspotReport {
61
+ profile: ModelProfile;
62
+ total: number;
63
+ /** Input cost of one call at the model's price; null without a price. */
64
+ costPerCall: number | null;
65
+ /** Re-sent on every call: system + tools + examples. */
66
+ fixedOverhead: number;
67
+ breakdown: Breakdown[];
68
+ /** Average tokens the conversation grows by per user turn. */
69
+ growthPerTurn: number;
70
+ /** User turns left before the context window is full, at the current growth. */
71
+ turnsLeft: number | null;
72
+ findings: Finding[];
73
+ }
74
+ export interface LazyTools {
75
+ /** The two tools to send instead of the full list. */
76
+ tools: ToolDef[];
77
+ /** Full definitions by name, for your runtime to return from get_tool_schema. */
78
+ schemas: Record<string, ToolDef>;
79
+ }
80
+ /**
81
+ * Load tool definitions on demand (the mcp-compressor pattern): the model sees
82
+ * a one-line catalog, fetches a tool's full schema with get_tool_schema when it
83
+ * needs it, and calls it through invoke_tool. Same tools, same model.
84
+ */
85
+ export declare function lazyTools(tools: ToolDef[]): LazyTools;
86
+ /**
87
+ * Prompt caching only reuses an identical opening, so a line that changes every
88
+ * call (a date, an id, the user's name) near the top makes everything after it
89
+ * full price. Moving those lines to the end keeps the same content, so the
90
+ * model reads the same facts.
91
+ */
92
+ export declare function stableFirst(text: string): {
93
+ text: string;
94
+ moved: string[];
95
+ };
96
+ /** A strict JSON schema from an example value: every field required, nothing extra. */
97
+ export declare function schemaFromExample(v: unknown): Record<string, unknown>;
98
+ /**
99
+ * Request settings that make the provider return JSON matching the schema,
100
+ * so no call is lost to a reply that doesn't parse. The schema comes from the
101
+ * JSON example in the prompt when there is one.
102
+ */
103
+ export declare function structuredOutputSetup(spec: AgentSpec, profile: ModelProfile): {
104
+ fromExample: boolean;
105
+ setup: Record<string, unknown>;
106
+ };
107
+ export declare function findHotspots(spec: AgentSpec, model: ModelProfile | string): HotspotReport;
108
+ export interface OptimizedAgent {
109
+ spec: AgentSpec;
110
+ report: HotspotReport;
111
+ before: number;
112
+ after: number;
113
+ /** Tokens and USD the applied fixes save per call (and per 1,000 calls). */
114
+ savings: Savings;
115
+ /** Evidence the optimized agent does the same work: what was compared and whether it held. */
116
+ verification: Verification;
117
+ }
118
+ /**
119
+ * Apply every safe fix that rewrites content (wording of the system prompt,
120
+ * tool descriptions and the next prompt; duplicate history) and return the
121
+ * new spec. Review fixes and advice-only fixes (caching) are left in the
122
+ * report for the user to opt into.
123
+ */
124
+ export declare function optimizeAgent(spec: AgentSpec, model: ModelProfile | string, level?: Level): OptimizedAgent;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Token Lens core: surface-agnostic. The browser extension uses it today; an
3
+ * API proxy, MCP server or editor plugin can call `analyze` the same way.
4
+ */
5
+ import { type Tip } from "./advice";
6
+ import { type ContextUsage } from "./budget";
7
+ import { type CompressResult, type Level } from "./compress";
8
+ import { type Savings } from "./cost";
9
+ import { type ModelPricing, type ModelProfile } from "./models";
10
+ import { type TaskInfo } from "./tasks";
11
+ export * from "./advice";
12
+ export * from "./budget";
13
+ export * from "./compress";
14
+ export * from "./cost";
15
+ export * from "./guard";
16
+ export * from "./hotspots";
17
+ export * from "./media";
18
+ export * from "./models";
19
+ export * from "./plans";
20
+ export * from "./redos";
21
+ export * from "./review";
22
+ export * from "./rules";
23
+ export * from "./tasks";
24
+ export * from "./tokens";
25
+ export * from "./verify";
26
+ export interface AnalyzeInput {
27
+ prompt: string;
28
+ /** A ModelProfile, or any model label/id to detect one from ("Claude Sonnet 4.5", "gpt-4o"). */
29
+ model: ModelProfile | string;
30
+ /** Site or provider id, used when the model label alone is ambiguous. */
31
+ site?: string;
32
+ /** Tokens already in the conversation (the history each new message re-reads). */
33
+ conversationTokens?: number;
34
+ level?: Level | "auto";
35
+ contextWindow?: number;
36
+ /** Your own price for this model (USD per 1M tokens); defaults to the API list price. */
37
+ pricing?: ModelPricing;
38
+ }
39
+ export interface CostImpact {
40
+ /** Price used, or undefined when the model has none. */
41
+ pricing?: ModelPricing;
42
+ /** Input cost of this prompt alone. */
43
+ promptUsd: number | null;
44
+ /** Input cost of sending it: the conversation it re-reads plus the prompt. */
45
+ sendUsd: number | null;
46
+ /** What the optimized prompt saves on this send. Later turns re-read the prompt, so it saves that again each turn. */
47
+ saved: Savings;
48
+ }
49
+ /**
50
+ * The prompt to paste: asks for the same answer in fewer tokens
51
+ * (meaning-preserving rewrite). Nothing is added that would change the answer.
52
+ */
53
+ export interface BestPrompt {
54
+ text: string;
55
+ tokens: number;
56
+ /** What changed, in plain words. */
57
+ changes: string[];
58
+ /** What the rewrite was required to keep, e.g. "2/2 numbers, 1/1 negations". */
59
+ kept: string;
60
+ }
61
+ export interface Analysis {
62
+ profile: ModelProfile;
63
+ task: TaskInfo;
64
+ promptTokens: number;
65
+ exact: boolean;
66
+ usage: ContextUsage;
67
+ level: Level;
68
+ optimized: CompressResult;
69
+ tips: Tip[];
70
+ handoffPrompt: string;
71
+ cost: CostImpact;
72
+ best: BestPrompt;
73
+ }
74
+ export declare function analyze(input: AnalyzeInput): Analysis;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Images and files: what an attachment costs in tokens before you send it.
3
+ *
4
+ * Providers resize images on their side before counting, so a bigger file is
5
+ * not always more tokens and a smaller file size (JPEG quality) never is. The
6
+ * count here follows each provider's published rules. Nothing suggests
7
+ * shrinking an image below what the provider uses, since fewer pixels can
8
+ * change the answer.
9
+ */
10
+ import type { ModelProfile } from "./models";
11
+ export interface ImageCost {
12
+ tokens: number;
13
+ usd: number | null;
14
+ /** Size the provider actually counts after its own resizing. */
15
+ sentAs: {
16
+ width: number;
17
+ height: number;
18
+ };
19
+ }
20
+ export declare function imageCost(width: number, height: number, profile: ModelProfile): ImageCost;
21
+ export interface PdfCost {
22
+ pages: number;
23
+ perPage: number;
24
+ tokens: number;
25
+ usd: number | null;
26
+ }
27
+ export declare function pdfCost(pages: number, profile: ModelProfile): PdfCost;
28
+ /** Page count of a PDF from its bytes (the page tree's /Count, else /Type /Page objects). */
29
+ export declare function pdfPageCount(bytes: Uint8Array): number;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Model profiles: what the plugin knows about each model family, and how it
3
+ * adapts to it. Profiles are matched from free text (the model picker label a
4
+ * chat site shows, an API model id, ...), so new versions of a family map to
5
+ * the right profile without a code change.
6
+ *
7
+ * Context windows are approximate defaults for the web apps; they vary by plan
8
+ * and change often, so users can override them in settings.
9
+ */
10
+ export type Family = "openai" | "anthropic" | "google" | "deepseek" | "mistral" | "xai" | "other";
11
+ export type TokenizerKind = "o200k" | "heuristic";
12
+ export interface ModelProfile {
13
+ id: string;
14
+ label: string;
15
+ family: Family;
16
+ /** Approximate context window in tokens. */
17
+ contextWindow: number;
18
+ /** Exact tokenizer if one is available offline, otherwise a heuristic. */
19
+ tokenizer: TokenizerKind;
20
+ /** Average characters per token on English prose, for the heuristic. */
21
+ charsPerToken: number;
22
+ /** Thinks before answering: hidden reasoning tokens, no need for "think step by step". */
23
+ reasoning: boolean;
24
+ /** Cheaper/faster sibling to suggest for simple asks, if any. */
25
+ lighterAlternative?: string;
26
+ /** Family-specific advice shown alongside the counts. */
27
+ notes?: string[];
28
+ /** USD per million tokens, when known. Set for custom or overridden models. */
29
+ pricing?: ModelPricing;
30
+ }
31
+ export interface ModelPricing {
32
+ inputPerMTok: number;
33
+ outputPerMTok: number;
34
+ cachedInputPerMTok?: number;
35
+ /** Some models bill the whole request at a higher rate once its input passes a size. */
36
+ longContext?: {
37
+ aboveTokens: number;
38
+ inputPerMTok: number;
39
+ outputPerMTok: number;
40
+ };
41
+ }
42
+ export declare const PROFILES: Record<string, ModelProfile>;
43
+ /** The matching rules, for ports of this list (the Python library reads them from a generated file). */
44
+ export declare const MODEL_RULES: ReadonlyArray<readonly [RegExp, string]>;
45
+ /** Fallback profile when a site is known but its model label can't be read. */
46
+ export declare const SITE_DEFAULTS: Record<string, string>;
47
+ /**
48
+ * A model the built-in list doesn't know, such as a company's own LLM, or a
49
+ * built-in family overridden with exact numbers (context window, pricing).
50
+ */
51
+ export interface CustomModel {
52
+ id: string;
53
+ label?: string;
54
+ /**
55
+ * Model ids or labels this profile applies to. A plain string matches as a
56
+ * case-insensitive substring ("acme-llm" matches "acme-llm-70b-instruct");
57
+ * "/.../" is a regular expression.
58
+ */
59
+ match?: string[];
60
+ family?: Family;
61
+ contextWindow: number;
62
+ tokenizer?: TokenizerKind;
63
+ charsPerToken?: number;
64
+ reasoning?: boolean;
65
+ lighterAlternative?: string;
66
+ notes?: string[];
67
+ pricing?: ModelPricing;
68
+ }
69
+ /**
70
+ * Add or replace a model profile. Custom models are matched before the
71
+ * built-in rules, so they can also override a built-in family.
72
+ */
73
+ export declare function registerModel(model: CustomModel): ModelProfile;
74
+ /** Remove a custom model's matching rules (and its profile, unless it is built in). */
75
+ export declare function unregisterModel(id: string): void;
76
+ /**
77
+ * Pick the profile for a model label such as "ChatGPT 5 Thinking",
78
+ * "Claude Opus 4.1" or "2.5 Flash". `siteId` breaks ties when the label alone
79
+ * doesn't name a family (Gemini's picker just says "2.5 Pro").
80
+ */
81
+ export declare function detectProfile(hint: string | null | undefined, siteId?: string): ModelProfile;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Subscription plans: how each chat app meters the plan the user is on, and
3
+ * how to stretch that allowance on the model in use. Never by switching model.
4
+ *
5
+ * Apps meter in two ways:
6
+ * messages each send counts once, whatever its length (ChatGPT's per-model
7
+ * caps, Gemini's daily prompt limits). Fewer, fuller messages help.
8
+ * compute usage grows with the tokens each message makes the model read
9
+ * and write (Claude's 5-hour and weekly limits). Long chats burn
10
+ * the allowance fastest, because every message re-reads them.
11
+ *
12
+ * Providers rarely publish exact caps. Where a number is known it carries its
13
+ * source and date and is shown as an estimate; the site's own usage warnings,
14
+ * when it shows them, take precedence, and users can correct either.
15
+ */
16
+ import type { ModelProfile } from "./models";
17
+ export type Meter = "messages" | "compute";
18
+ export interface PlanCap {
19
+ /** Model profile ids the cap covers. */
20
+ models: string[];
21
+ messages: number;
22
+ window: "3h" | "5h" | "day" | "week";
23
+ source: string;
24
+ asOf: string;
25
+ }
26
+ export interface Plan {
27
+ id: string;
28
+ site: "chatgpt" | "claude" | "gemini" | "other";
29
+ label: string;
30
+ meter: Meter;
31
+ caps: PlanCap[];
32
+ note?: string;
33
+ }
34
+ export declare const PLANS: Record<string, Plan>;
35
+ /** Plan from the account UI text (sidebar, profile menu), e.g. "Max plan", "Plus", "Google AI Pro". */
36
+ export declare function detectPlan(hint: string, site: string): Plan | null;
37
+ /** What the site itself says about usage: a limit warning, messages left, or percent used. */
38
+ export interface SiteMeter {
39
+ text: string;
40
+ messagesLeft?: number;
41
+ percentUsed?: number;
42
+ resetsAt?: string;
43
+ limited: boolean;
44
+ /** Weekly limit used, when the site reports one separately from the session. */
45
+ weeklyPercentUsed?: number;
46
+ /** "account": read from the site's own usage meter for your account; "page": a banner on the page. */
47
+ source?: "account" | "page";
48
+ }
49
+ /**
50
+ * Claude's own usage meter for your account (the numbers on Settings > Usage):
51
+ * the current session and the weekly limit, as percent used.
52
+ */
53
+ export declare function readClaudeUsage(json: unknown): SiteMeter | null;
54
+ /** Parse a usage banner the site shows ("5 messages remaining until 6 PM", "80% of your weekly limit used"). */
55
+ export declare function readSiteMeter(text: string): SiteMeter | null;
56
+ export interface AllowanceInput {
57
+ plan: Plan | null;
58
+ site: string;
59
+ profile: ModelProfile;
60
+ /** Tokens already in the chat that this message re-reads. */
61
+ conversationTokens: number;
62
+ promptTokens: number;
63
+ /** Messages sent on this model in each window, counted locally. */
64
+ sent?: Partial<Record<PlanCap["window"], number>>;
65
+ siteMeter?: SiteMeter | null;
66
+ }
67
+ export interface Allowance {
68
+ meter: Meter;
69
+ plan: Plan | null;
70
+ /** One line on what this message uses of the allowance. */
71
+ share: string;
72
+ /** Model-specific ways to stretch it; none switch the model. */
73
+ tips: Array<{
74
+ id: string;
75
+ text: string;
76
+ action?: "handoff" | "append";
77
+ append?: string;
78
+ }>;
79
+ cap?: PlanCap & {
80
+ used: number;
81
+ left: number;
82
+ };
83
+ /** What the site itself shows about usage, if anything. */
84
+ siteMeter?: SiteMeter | null;
85
+ /** compute meter: how many times more this message reads than it would in a fresh chat with a handoff brief. */
86
+ freshChatMultiple?: number;
87
+ }
88
+ export declare function allowance(input: AllowanceInput): Allowance;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Redos: tokens spent on answers that were thrown away, either regenerated
3
+ * (the site's "2 / 2" arrows) or re-asked ("no, I meant ..."), and the single
4
+ * prompt that would likely have worked the first time.
5
+ *
6
+ * A redo costs a whole turn: the history it re-read plus the reply it
7
+ * produced. The suggested prompt only joins what the user already said across
8
+ * the attempts; it adds nothing new.
9
+ */
10
+ import type { Message } from "./hotspots";
11
+ import type { ModelProfile } from "./models";
12
+ export interface Redo {
13
+ kind: "regenerate" | "reask";
14
+ /** Index of the user message whose answer was redone. */
15
+ index: number;
16
+ preview: string;
17
+ /** Extra attempts beyond the first. */
18
+ extra: number;
19
+ tokens: number;
20
+ usd: number | null;
21
+ }
22
+ export interface RedoReport {
23
+ redos: Redo[];
24
+ tokens: number;
25
+ usd: number | null;
26
+ /** One prompt carrying everything said across the attempts of the costliest re-ask. */
27
+ firstTimePrompt?: string;
28
+ }
29
+ /**
30
+ * @param regenerations assistant message index -> how many versions the site shows (e.g. 3 for "3 / 3").
31
+ */
32
+ export declare function findRedos(messages: Message[], profile: ModelProfile, regenerations?: Record<number, number>): RedoReport;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Chat review: looks back over the messages already sent in a conversation and
3
+ * shows where tokens could have been saved, and what that would have cost.
4
+ *
5
+ * A message sent in turn i is re-read as history by every later turn, so a
6
+ * shorter message saves its difference once when sent plus once per later
7
+ * turn. Only meaning-preserving rewrites count toward the saving (the same
8
+ * compress + meaning guard used for new prompts); tips that could change the
9
+ * answer (length caps, pasting less) are listed separately, never counted.
10
+ */
11
+ import { type Tip } from "./advice";
12
+ import { type Level } from "./compress";
13
+ import { type Savings } from "./cost";
14
+ import type { Message } from "./hotspots";
15
+ import type { ModelProfile } from "./models";
16
+ import { type Task } from "./tasks";
17
+ export interface ReviewedMessage {
18
+ /** Position in the conversation (0-based, all roles). */
19
+ index: number;
20
+ /** First words of the message. */
21
+ preview: string;
22
+ task: Task;
23
+ tokens: number;
24
+ /** Tokens the meaning-preserving rewrite saves on one read. */
25
+ shorterBy: number;
26
+ rules: string[];
27
+ /** Later turns that re-read this message. */
28
+ rereads: number;
29
+ /** Total saving: shorterBy × (1 + rereads), priced for the model. */
30
+ saved: Savings;
31
+ /** What to do differently next time (may change the answer, so never counted). */
32
+ tips: Tip[];
33
+ }
34
+ export interface ChatReview {
35
+ messages: ReviewedMessage[];
36
+ /** Total over all user messages, including re-reads. */
37
+ saved: Savings;
38
+ /** User messages reviewed. */
39
+ reviewed: number;
40
+ }
41
+ export interface ReviewOptions {
42
+ level?: Level;
43
+ /** Cache of per-message results keyed by text, so long chats aren't re-analysed every refresh. */
44
+ cache?: Map<string, Omit<ReviewedMessage, "index" | "rereads" | "saved">>;
45
+ }
46
+ export declare function reviewChat(messages: Message[], profile: ModelProfile, opts?: ReviewOptions): ChatReview;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Coding-agent rule files (CLAUDE.md, AGENTS.md, .cursorrules, copilot
3
+ * instructions, ...) are re-sent on every turn, so every line costs once per
4
+ * turn. This audit finds repeated lines (within and across files) and wording
5
+ * that can be tightened without losing meaning, and flags references to files
6
+ * that no longer exist.
7
+ *
8
+ * Only exact repeats and meaning-preserving wording go into the trimmed files,
9
+ * so the agent gets the same instructions; stale references are only listed,
10
+ * since removing an instruction can change what the agent does.
11
+ */
12
+ import { type Savings } from "./cost";
13
+ import type { ModelProfile } from "./models";
14
+ export interface RuleFile {
15
+ path: string;
16
+ content: string;
17
+ }
18
+ export interface RuleFinding {
19
+ kind: "duplicate" | "stale-path";
20
+ path: string;
21
+ line: number;
22
+ text: string;
23
+ detail: string;
24
+ /** Tokens per turn this line costs. */
25
+ tokens: number;
26
+ /** True when the trimmed file already drops it. */
27
+ applied: boolean;
28
+ }
29
+ export interface RuleFileResult {
30
+ path: string;
31
+ tokens: number;
32
+ trimmed: string;
33
+ trimmedTokens: number;
34
+ }
35
+ export interface RulesAudit {
36
+ files: RuleFileResult[];
37
+ findings: RuleFinding[];
38
+ before: number;
39
+ after: number;
40
+ /** Saving per turn from the trimmed files (repeats and wording only). */
41
+ saved: Savings;
42
+ /** Tokens per turn on lines that mention missing files (listed, not removed). */
43
+ stale: number;
44
+ }
45
+ export declare function auditRules(files: RuleFile[], profile: ModelProfile, opts?: {
46
+ knownPaths?: string[];
47
+ }): RulesAudit;