/** * The interception layer: compression on the wire. * * WHY A PROXY AT ALL, when this package is built out of hooks. Because a hook * provably cannot do this. `docs/superpowers/spikes/2026-08-30-posttooluse- * rewrite.md` established that the `PostToolUse` output schema is * `{hookEventName, additionalContext?, classifierContext?}` and that * `updatedOutput`, `modifiedOutput`, `toolResponse`, `updatedResult` and * `replaceOutput` occur ZERO times in the Claude Code binary. A hook can add * context; it cannot replace a tool result. * * THIS NEVER TRIES TO. It rewrites the outbound REQUEST, and tool results are * already inside that request as conversation history -- so the bytes a hook * cannot touch at PostToolUse are fully rewritable one moment later, here. * That is the entire idea, and it is why this reaches results from other * vendors' MCP servers, WebFetch bodies, and the history that grows every turn, * none of which a hook can see. * * WHAT IT WILL NOT DO: * - bind anywhere but loopback; * - store, log or persist a payload; * - read or retain a credential -- headers are forwarded verbatim; * - rewrite a message carrying signed thinking blocks (a permanent 400); * - touch anything at or before the last cache breakpoint; * - run at all unless explicitly enabled. */ import { type Server } from 'node:http'; import { type AnchorStore } from '../compress/anchor.js'; import type { Finding } from '../compress/knowledge.js'; import { type CompressionOptions, type PresetName, type Tuning } from '../compress/options.js'; /** * May the default upstream serve this path? * * Exported so the rule can be tested for what it ADMITS as well as what it refuses -- * the refusal is easy to exercise end to end, while proving `/v1/messages` still gets * through would otherwise mean letting a test reach api.anthropic.com. */ export declare function defaultUpstreamServes(path: string): boolean; /** True when the upstream was chosen for us rather than by the operator. */ export declare function upstreamIsDefault(options: ProxyOptions): boolean; export interface ProxyOptions { readonly port?: number; readonly upstream?: string; /** Called with a one-line summary per request. Never receives payloads. */ readonly onSummary?: (summary: ProxySummary) => void; /** * Where to look for this project's knowledge graph. * * Defaults to the directory the proxy was started in, which is where the * rest of this package resolves a project from. */ readonly projectRoot?: string; /** Disable graph injection when a client chooses its working tree after launch. */ readonly knowledge?: boolean; /** Named starting point for the dials. Defaults to the environment's. */ readonly preset?: PresetName | string; /** Expert overrides, layered over the preset. */ readonly compression?: CompressionOptions; /** * Largest request body to buffer, in bytes. * * Only ever lowers the built-in ceiling; a larger value is ignored, because raising it * would opt back into the unbounded buffering the ceiling exists to prevent. */ readonly maxBodyBytes?: number; } export interface ProxySummary { readonly path: string; readonly beforeBytes: number; readonly afterBytes: number; readonly compressed: boolean; readonly reason?: string; /** * Characters of knowledge deliberately ADDED to the request. * * Reported separately from the byte counts because it is the opposite of * what the rest of this does, and a summary that hid it would make the * compression figure beside it a lie. */ readonly injectedChars?: number; /** * Which branch the anchor decision took, and how many elisions the * strategy produced. * * DIAGNOSTIC, AND IT EXISTS BECAUSE INFERENCE FAILED TWICE. Two campaigns * reported 'compression did not pay' on every request, and reading the code * produced two confident explanations that a local reproduction then * disproved. These two fields separate the remaining possibilities without * guessing: no elisions means nothing was touchable or no engine claimed the * content, while elisions with no saving means the rewrite grew the payload. * Neither records any payload. */ readonly anchorReason?: string; readonly elisions?: number; /** Section sizes, for locating where a request's bytes live. No content. */ readonly deferredTools?: number; /** Probe: thinking blocks removed from older assistant turns. */ readonly droppedThinking?: number; readonly deferredToolChars?: number; readonly systemChars?: number; readonly toolsChars?: number; readonly toolCount?: number; readonly mcpToolChars?: number; readonly topTools?: string; readonly coreToolChars?: number; readonly messagesChars?: number; readonly messageCount?: number; } /** Enabled by default; explicit opt-outs and the global kill switch win. */ export declare function proxyEnabled(env?: NodeJS.ProcessEnv): boolean; /** The limit in force, which a caller may lower but never raise past the default. */ export declare function bodyLimitFor(options: ProxyOptions): number; /** * Compresses a provider request body, or explains why it did not. * * FAIL OPEN, ALWAYS. Every branch that cannot proceed returns the original * bytes. A token optimizer that wedges the agent is worse than one that saves * nothing -- the same rule the hook path has followed since it was written. */ export declare function compressBody(body: Buffer, spill: (content: string, hint: string) => string, anchors?: AnchorStore, findings?: readonly Finding[], tuning?: Tuning, /** True when `findings` came from a graph shared across projects. */ sharedGraph?: boolean, wireFormat?: 'chat-completions'): { body: Buffer; summary: Omit; }; export declare function knowledgeEnabled(env: NodeJS.ProcessEnv): boolean; /** * How many characters of established knowledge go into the cached prefix. * * A KNOB FOR THE SAME REASON THE TOOL APERTURE IS ONE. The default of 2,000 * fits six to eight lines out of 72,736 characters of eligible claim text -- * 2.7% of what this project has worked out -- and that number was never chosen * against a measurement, only against a worry that a longer block would bury * the relevant lines. The block is charged once as a cache write and then read * at 0.1x, so widening it is cheap per turn and expensive only once; whether it * buys turns is a question for the rig, not for an argument. * * Returns undefined when unset so the preset's own value survives; falling * back to the constant here would silently override a preset that chose a * different budget on purpose. */ export declare function knowledgeCharsFromEnv(env?: NodeJS.ProcessEnv): number | undefined; /** * How many large tool definitions stay loaded when deferral is on. * * A KNOB BECAUSE THE RIGHT VALUE IS MEASURED, NOT REASONED. The default of * five was a guess; the first campaign to run deferral end to end deferred 14 * of 26 definitions, cut the model's context from 29,824 prefix tokens to * 19,138 -- below the 20,431 of an arm with no proxy at all -- and the * transcripts show the tool search tool was never once invoked. Nothing was * searched for, so the guess was too cautious, and finding out how much too * cautious costs a campaign rather than an argument. * * Out-of-range and unparseable values fall back to the default rather than * throwing: a typo in an environment variable must not take a session down. */ export declare function keepToolsFromEnv(env?: NodeJS.ProcessEnv): number; /** Deferral defaults on unless the environment explicitly disables it. */ export declare function deferToolsEnabled(env?: NodeJS.ProcessEnv): boolean; /** * Below how many characters a tool definition is exempt from deferral. * The previous default exempted definitions below 1,500 characters to avoid * discovery round trips. Captured traffic showed that 88 of 115 real tools * fell under that floor, leaving about 55 KB undeferred; tool definitions were * 66.9% of the request. That aggregate cost motivated removing the default floor. * * `SMALL_TOOL_CHARS` is now zero: every eligible definition may be deferred. * TOKEN_OPTIMIZER_PROXY_SMALL_TOOL_CHARS can restore a positive exemption for * workloads where discovery round trips cost more than the saved definition. */ export declare function smallToolCharsFromEnv(env?: NodeJS.ProcessEnv): number; /** * Is it safe to send credentials to this upstream? * * CREDENTIALS DO NOT GO OVER CLEARTEXT. Every request through here carries * the user's provider key in a header, forwarded verbatim -- so an upstream * of `http://api.example.com` would put that key on the wire in the clear, * and the override that sets it is a single environment variable. * * Loopback is the exception, and it has to be: the tests in this repository * run a stand-in provider on 127.0.0.1, and so does anyone debugging with a * local recorder. Traffic that never leaves the machine cannot be * intercepted on the way to somewhere else. */ export declare function upstreamIsSafe(upstream: string): boolean; /** * The path and query of a request, and NOTHING a caller can aim with. * * THE UPSTREAM IS OURS TO CHOOSE, NOT THE REQUEST'S. HTTP lets a client send * an absolute-form target -- `POST http://elsewhere.example/v1/messages` -- * and a proxy is exactly the thing that form exists for, so it arrives here * legitimately shaped. `new URL(req.url, upstream)` then IGNORES the base * entirely and resolves to the host in the target, which would send the * user's provider key to whoever asked. A protocol-relative target * (`//elsewhere.example/...`) does the same while keeping our scheme. * * Validating the configured upstream, as `startProxy` does, does not help: * the destination was never read from it. So the request contributes only a * path and a query string, and anything carrying a scheme or an authority is * refused rather than quietly stripped -- a client sending one is either * confused about what this is or is aiming it, and both are worth saying out * loud. */ export declare function requestPath(url: string | undefined): string | null; /** Starts the proxy. Resolves once it is listening. */ export declare function startProxy(options?: ProxyOptions): Promise<{ server: Server; port: number; }>; //# sourceMappingURL=server.d.ts.map