@compaction/cli 0.1.4 → 0.3.0

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 (170) hide show
  1. package/README.md +51 -12
  2. package/dist/cli/commands/activity.d.ts +8 -0
  3. package/dist/cli/commands/activity.js +35 -0
  4. package/dist/cli/commands/apply-context.js +68 -0
  5. package/dist/cli/commands/billing-delta.js +12 -1
  6. package/dist/cli/commands/capture-claude-code.d.ts +44 -0
  7. package/dist/cli/commands/capture-claude-code.js +206 -0
  8. package/dist/cli/commands/capture.js +283 -1
  9. package/dist/cli/commands/compact.js +24 -11
  10. package/dist/cli/commands/context.d.ts +2 -0
  11. package/dist/cli/commands/context.js +130 -0
  12. package/dist/cli/commands/dev.d.ts +17 -0
  13. package/dist/cli/commands/dev.js +131 -0
  14. package/dist/cli/commands/gateway.d.ts +31 -0
  15. package/dist/cli/commands/gateway.js +312 -0
  16. package/dist/cli/commands/hooks.d.ts +5 -0
  17. package/dist/cli/commands/hooks.js +181 -0
  18. package/dist/cli/commands/import.js +3 -1
  19. package/dist/cli/commands/init.js +422 -17
  20. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  21. package/dist/cli/commands/input-compaction-ab.js +125 -0
  22. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  23. package/dist/cli/commands/optimize-hosted.js +123 -0
  24. package/dist/cli/commands/optimize.js +8 -0
  25. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  26. package/dist/cli/commands/output-shaping-ab.js +132 -0
  27. package/dist/cli/commands/output-shaping.d.ts +7 -0
  28. package/dist/cli/commands/output-shaping.js +43 -0
  29. package/dist/cli/commands/policies.d.ts +2 -0
  30. package/dist/cli/commands/policies.js +80 -0
  31. package/dist/cli/commands/precall.d.ts +19 -0
  32. package/dist/cli/commands/precall.js +436 -0
  33. package/dist/cli/commands/recommend.js +1 -1
  34. package/dist/cli/commands/run.js +310 -18
  35. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  36. package/dist/cli/commands/upgrade-status.js +152 -0
  37. package/dist/cli/cursor-export-read.d.ts +13 -0
  38. package/dist/cli/cursor-export-read.js +53 -0
  39. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  40. package/dist/cli/cursor-live-preflight.js +46 -0
  41. package/dist/cli/index.js +39 -2
  42. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  43. package/dist/cli/onboarding/GatewayTui.js +113 -0
  44. package/dist/cli/onboarding/model.d.ts +32 -1
  45. package/dist/cli/onboarding/model.js +27 -2
  46. package/dist/cli/terminal-logo.d.ts +27 -0
  47. package/dist/cli/terminal-logo.js +49 -0
  48. package/dist/core/activity-event.d.ts +107 -0
  49. package/dist/core/activity-event.js +182 -0
  50. package/dist/core/activity-store.d.ts +63 -0
  51. package/dist/core/activity-store.js +254 -0
  52. package/dist/core/activity-view.d.ts +76 -0
  53. package/dist/core/activity-view.js +120 -0
  54. package/dist/core/api-client/index.d.ts +3 -1
  55. package/dist/core/api-client/index.js +4 -0
  56. package/dist/core/api-client/payload.d.ts +5 -1
  57. package/dist/core/api-client/payload.js +8 -6
  58. package/dist/core/api-client/persisted-config.d.ts +57 -0
  59. package/dist/core/api-client/persisted-config.js +156 -0
  60. package/dist/core/api-client/tool.d.ts +29 -0
  61. package/dist/core/api-client/tool.js +50 -0
  62. package/dist/core/api-client/types.d.ts +15 -4
  63. package/dist/core/auto-apply-ask.d.ts +58 -0
  64. package/dist/core/auto-apply-ask.js +105 -0
  65. package/dist/core/auto-apply-gates.d.ts +76 -0
  66. package/dist/core/auto-apply-gates.js +113 -0
  67. package/dist/core/before-call-activity.d.ts +35 -0
  68. package/dist/core/before-call-activity.js +103 -0
  69. package/dist/core/before-call-recovery.d.ts +19 -0
  70. package/dist/core/before-call-recovery.js +46 -0
  71. package/dist/core/before-call-stdin.d.ts +59 -0
  72. package/dist/core/before-call-stdin.js +78 -0
  73. package/dist/core/before-call.d.ts +149 -0
  74. package/dist/core/before-call.js +358 -0
  75. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  76. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  77. package/dist/core/capture-record.d.ts +40 -0
  78. package/dist/core/capture-record.js +55 -0
  79. package/dist/core/claude-code-before-call.d.ts +37 -0
  80. package/dist/core/claude-code-before-call.js +120 -0
  81. package/dist/core/claude-code-connect.d.ts +31 -0
  82. package/dist/core/claude-code-connect.js +87 -0
  83. package/dist/core/claude-code-hook-record.d.ts +71 -0
  84. package/dist/core/claude-code-hook-record.js +0 -0
  85. package/dist/core/claude-code-hooks.d.ts +77 -0
  86. package/dist/core/claude-code-hooks.js +141 -0
  87. package/dist/core/codex-capture.d.ts +45 -0
  88. package/dist/core/codex-capture.js +204 -0
  89. package/dist/core/command-runner.js +5 -1
  90. package/dist/core/compaction-artifacts.d.ts +51 -0
  91. package/dist/core/compaction-artifacts.js +242 -0
  92. package/dist/core/compactor.d.ts +9 -0
  93. package/dist/core/compactor.js +95 -0
  94. package/dist/core/context-store-eval-cases.d.ts +6 -0
  95. package/dist/core/context-store-eval-cases.js +331 -0
  96. package/dist/core/context-store-eval.d.ts +140 -0
  97. package/dist/core/context-store-eval.js +138 -0
  98. package/dist/core/context-store-fs.d.ts +73 -0
  99. package/dist/core/context-store-fs.js +157 -0
  100. package/dist/core/context-store-sufficiency.d.ts +98 -0
  101. package/dist/core/context-store-sufficiency.js +135 -0
  102. package/dist/core/context-store.d.ts +155 -0
  103. package/dist/core/context-store.js +228 -0
  104. package/dist/core/cross-surface-event.d.ts +306 -0
  105. package/dist/core/cross-surface-event.js +330 -0
  106. package/dist/core/cursor-capture.d.ts +54 -0
  107. package/dist/core/cursor-capture.js +215 -0
  108. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  109. package/dist/core/cursor-preflight-probe.js +88 -0
  110. package/dist/core/cursor-preflight.d.ts +86 -0
  111. package/dist/core/cursor-preflight.js +126 -0
  112. package/dist/core/gateway/apply-activation.d.ts +39 -0
  113. package/dist/core/gateway/apply-activation.js +84 -0
  114. package/dist/core/gateway/apply-policy.d.ts +64 -0
  115. package/dist/core/gateway/apply-policy.js +221 -0
  116. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  117. package/dist/core/gateway/apply-receipt.js +75 -0
  118. package/dist/core/gateway/cache-proof.d.ts +45 -0
  119. package/dist/core/gateway/cache-proof.js +65 -0
  120. package/dist/core/gateway/configure.d.ts +50 -0
  121. package/dist/core/gateway/configure.js +169 -0
  122. package/dist/core/gateway/openai-usage.d.ts +56 -0
  123. package/dist/core/gateway/openai-usage.js +128 -0
  124. package/dist/core/gateway/receipt.d.ts +138 -0
  125. package/dist/core/gateway/receipt.js +120 -0
  126. package/dist/core/gateway/recovery.d.ts +23 -0
  127. package/dist/core/gateway/recovery.js +68 -0
  128. package/dist/core/gateway/server.d.ts +51 -0
  129. package/dist/core/gateway/server.js +276 -0
  130. package/dist/core/gateway/status.d.ts +45 -0
  131. package/dist/core/gateway/status.js +109 -0
  132. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  133. package/dist/core/hook-usage-aggregate.js +161 -0
  134. package/dist/core/input-compaction-ab.d.ts +111 -0
  135. package/dist/core/input-compaction-ab.js +158 -0
  136. package/dist/core/local-run-record.d.ts +109 -0
  137. package/dist/core/local-run-record.js +223 -0
  138. package/dist/core/output-shaping-ab.d.ts +140 -0
  139. package/dist/core/output-shaping-ab.js +146 -0
  140. package/dist/core/output-shaping-attach.d.ts +31 -0
  141. package/dist/core/output-shaping-attach.js +57 -0
  142. package/dist/core/output-shaping.d.ts +56 -0
  143. package/dist/core/output-shaping.js +89 -0
  144. package/dist/core/policy-middleware.d.ts +121 -0
  145. package/dist/core/policy-middleware.js +919 -0
  146. package/dist/core/policy-preferences.d.ts +99 -0
  147. package/dist/core/policy-preferences.js +232 -0
  148. package/dist/core/report-generator.d.ts +19 -1
  149. package/dist/core/report-generator.js +51 -0
  150. package/dist/core/run-aggregator.d.ts +56 -1
  151. package/dist/core/run-aggregator.js +93 -2
  152. package/dist/core/run-flow-report.d.ts +82 -0
  153. package/dist/core/run-flow-report.js +71 -0
  154. package/dist/core/safety-report.js +8 -1
  155. package/dist/core/shim-capture-bridge.d.ts +32 -0
  156. package/dist/core/shim-capture-bridge.js +88 -0
  157. package/dist/core/skill-injection-policy.d.ts +72 -0
  158. package/dist/core/skill-injection-policy.js +183 -0
  159. package/dist/core/spend-attribution.js +1 -1
  160. package/dist/core/token-accounting.d.ts +1 -1
  161. package/dist/core/tool-shim.d.ts +129 -0
  162. package/dist/core/tool-shim.js +447 -0
  163. package/dist/core/trace-adapters.d.ts +8 -0
  164. package/dist/core/trace-adapters.js +4 -0
  165. package/dist/core/trace-parser.d.ts +13 -13
  166. package/dist/core/trace-parser.js +6 -6
  167. package/dist/core/types.d.ts +45 -2
  168. package/dist/core/waste-detector.d.ts +20 -0
  169. package/dist/core/waste-detector.js +160 -6
  170. package/package.json +1 -1
@@ -0,0 +1,56 @@
1
+ /**
2
+ * OpenAI usage parsing for the Compaction Gateway record mode (PUBLIC CLI/SDK code — engine-free,
3
+ * ships in the npm package). Charter 2026-07-04-run-15 — the first gateway implementation.
4
+ *
5
+ * This module is PURE and CONTENT-FREE by construction: it takes a response body (a non-streaming JSON
6
+ * body, or the tail of an SSE stream) and extracts ONLY the token-usage COUNTS + the model label. It
7
+ * never returns, stores, or logs any message / completion / tool text — only the numbers the provider
8
+ * itself reports in its `usage` object, plus the model name (metadata, not content).
9
+ *
10
+ * OpenAI usage shape (Chat Completions / Responses):
11
+ * usage.prompt_tokens — total input tokens the provider billed for
12
+ * usage.prompt_tokens_details.cached_tokens — the portion served from the provider prompt cache
13
+ * usage.completion_tokens — output tokens
14
+ * usage.completion_tokens_details.reasoning_tokens — reasoning/output-detail tokens (o-series), where present
15
+ *
16
+ * Honesty: a field is reported ONLY when the provider actually returned it. A missing axis is
17
+ * `unavailable` with a reason — NEVER a silent zero. `billed_fresh_input_tokens = prompt - cached`
18
+ * (the input the provider billed at full rate) is computed ONLY when BOTH prompt and cached are present.
19
+ */
20
+ /** A normalized, content-free token breakdown from a provider `usage` object. */
21
+ export interface OpenAiUsageBreakdown {
22
+ /** True when a `usage` object with a numeric `prompt_tokens` (or output) was found. */
23
+ present: boolean;
24
+ /** Total input tokens the provider reports (usage.prompt_tokens), when present. */
25
+ promptInputTokens?: number;
26
+ /** Input tokens served from the provider prompt cache (prompt_tokens_details.cached_tokens), when present. */
27
+ cachedInputTokens?: number;
28
+ /** Fresh/billed-at-full-rate input tokens = prompt - cached (only when BOTH are present). */
29
+ billedFreshInputTokens?: number;
30
+ /** Output tokens (usage.completion_tokens), when present. */
31
+ outputTokens?: number;
32
+ /** Reasoning/output-detail tokens (completion_tokens_details.reasoning_tokens), when present. */
33
+ reasoningTokens?: number;
34
+ /** The model label the provider echoed (metadata, NOT content), when present. */
35
+ model?: string;
36
+ /** Honest reason the usage is unavailable, when `present` is false. */
37
+ unavailableReason?: string;
38
+ }
39
+ /**
40
+ * Extract the content-free usage breakdown from an already-parsed OpenAI response object. Handles BOTH
41
+ * OpenAI APIs (their usage field names differ):
42
+ * - Chat Completions: `usage.prompt_tokens` / `completion_tokens` / `prompt_tokens_details.cached_tokens`
43
+ * / `completion_tokens_details.reasoning_tokens`.
44
+ * - Responses API: `usage.input_tokens` / `output_tokens` / `input_tokens_details.cached_tokens`
45
+ * / `output_tokens_details.reasoning_tokens`. Its SSE events wrap the payload under `response`
46
+ * (`{"type":"response.completed","response":{usage,model}}`), so we also look one level down.
47
+ */
48
+ export declare function usageFromResponseObject(obj: unknown): OpenAiUsageBreakdown;
49
+ /**
50
+ * Extract usage from a raw response body string. Handles BOTH shapes byte-safely (it only READS a copy):
51
+ * - non-streaming JSON: `JSON.parse(body).usage`.
52
+ * - streaming SSE: the LAST `data:` line carrying a `usage` object (present only when the caller sent
53
+ * `stream_options.include_usage: true`). The `[DONE]` sentinel is ignored.
54
+ * Any parse failure → `present: false` with an honest reason (never throws; the proxy stays byte-safe).
55
+ */
56
+ export declare function usageFromResponseBody(body: string): OpenAiUsageBreakdown;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * OpenAI usage parsing for the Compaction Gateway record mode (PUBLIC CLI/SDK code — engine-free,
3
+ * ships in the npm package). Charter 2026-07-04-run-15 — the first gateway implementation.
4
+ *
5
+ * This module is PURE and CONTENT-FREE by construction: it takes a response body (a non-streaming JSON
6
+ * body, or the tail of an SSE stream) and extracts ONLY the token-usage COUNTS + the model label. It
7
+ * never returns, stores, or logs any message / completion / tool text — only the numbers the provider
8
+ * itself reports in its `usage` object, plus the model name (metadata, not content).
9
+ *
10
+ * OpenAI usage shape (Chat Completions / Responses):
11
+ * usage.prompt_tokens — total input tokens the provider billed for
12
+ * usage.prompt_tokens_details.cached_tokens — the portion served from the provider prompt cache
13
+ * usage.completion_tokens — output tokens
14
+ * usage.completion_tokens_details.reasoning_tokens — reasoning/output-detail tokens (o-series), where present
15
+ *
16
+ * Honesty: a field is reported ONLY when the provider actually returned it. A missing axis is
17
+ * `unavailable` with a reason — NEVER a silent zero. `billed_fresh_input_tokens = prompt - cached`
18
+ * (the input the provider billed at full rate) is computed ONLY when BOTH prompt and cached are present.
19
+ */
20
+ function num(value) {
21
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
22
+ }
23
+ function obj_(value) {
24
+ return value && typeof value === "object" ? value : undefined;
25
+ }
26
+ /**
27
+ * Extract the content-free usage breakdown from an already-parsed OpenAI response object. Handles BOTH
28
+ * OpenAI APIs (their usage field names differ):
29
+ * - Chat Completions: `usage.prompt_tokens` / `completion_tokens` / `prompt_tokens_details.cached_tokens`
30
+ * / `completion_tokens_details.reasoning_tokens`.
31
+ * - Responses API: `usage.input_tokens` / `output_tokens` / `input_tokens_details.cached_tokens`
32
+ * / `output_tokens_details.reasoning_tokens`. Its SSE events wrap the payload under `response`
33
+ * (`{"type":"response.completed","response":{usage,model}}`), so we also look one level down.
34
+ */
35
+ export function usageFromResponseObject(obj) {
36
+ if (!obj || typeof obj !== "object") {
37
+ return { present: false, unavailableReason: "response body was not a JSON object" };
38
+ }
39
+ const root = obj;
40
+ // The Responses API SSE stream nests the completed response under `response`.
41
+ const nested = obj_(root.response);
42
+ const usage = obj_(root.usage) ?? obj_(nested?.usage);
43
+ const model = typeof root.model === "string" ? root.model : typeof nested?.model === "string" ? nested.model : undefined;
44
+ if (!usage) {
45
+ return {
46
+ present: false,
47
+ ...(model ? { model } : {}),
48
+ unavailableReason: "the provider response carried no usage object (e.g. a streamed response without usage/include_usage)"
49
+ };
50
+ }
51
+ // Accept both APIs' field names (Chat Completions ?? Responses API).
52
+ const promptInputTokens = num(usage.prompt_tokens) ?? num(usage.input_tokens);
53
+ const outputTokens = num(usage.completion_tokens) ?? num(usage.output_tokens);
54
+ const cachedInputTokens = num(obj_(usage.prompt_tokens_details)?.cached_tokens) ?? num(obj_(usage.input_tokens_details)?.cached_tokens);
55
+ const reasoningTokens = num(obj_(usage.completion_tokens_details)?.reasoning_tokens) ?? num(obj_(usage.output_tokens_details)?.reasoning_tokens);
56
+ if (promptInputTokens === undefined && outputTokens === undefined) {
57
+ return {
58
+ present: false,
59
+ ...(model ? { model } : {}),
60
+ unavailableReason: "the usage object carried no numeric input/prompt or output/completion tokens"
61
+ };
62
+ }
63
+ const billedFreshInputTokens = promptInputTokens !== undefined && cachedInputTokens !== undefined
64
+ ? Math.max(0, promptInputTokens - cachedInputTokens)
65
+ : undefined;
66
+ return {
67
+ present: true,
68
+ ...(promptInputTokens !== undefined ? { promptInputTokens } : {}),
69
+ ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
70
+ ...(billedFreshInputTokens !== undefined ? { billedFreshInputTokens } : {}),
71
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
72
+ ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
73
+ ...(model ? { model } : {})
74
+ };
75
+ }
76
+ /**
77
+ * Extract usage from a raw response body string. Handles BOTH shapes byte-safely (it only READS a copy):
78
+ * - non-streaming JSON: `JSON.parse(body).usage`.
79
+ * - streaming SSE: the LAST `data:` line carrying a `usage` object (present only when the caller sent
80
+ * `stream_options.include_usage: true`). The `[DONE]` sentinel is ignored.
81
+ * Any parse failure → `present: false` with an honest reason (never throws; the proxy stays byte-safe).
82
+ */
83
+ export function usageFromResponseBody(body) {
84
+ const trimmed = body.trim();
85
+ if (trimmed === "")
86
+ return { present: false, unavailableReason: "empty response body" };
87
+ // Non-streaming JSON first.
88
+ if (trimmed.startsWith("{")) {
89
+ try {
90
+ return usageFromResponseObject(JSON.parse(trimmed));
91
+ }
92
+ catch {
93
+ return { present: false, unavailableReason: "response body was not parseable JSON (may be truncated)" };
94
+ }
95
+ }
96
+ // Streaming SSE: scan for the LAST `data:` payload that parses and carries a usage object.
97
+ let best;
98
+ let lastModel;
99
+ for (const line of trimmed.split(/\r?\n/)) {
100
+ const s = line.trim();
101
+ if (!s.startsWith("data:"))
102
+ continue;
103
+ const payload = s.slice("data:".length).trim();
104
+ if (payload === "" || payload === "[DONE]")
105
+ continue;
106
+ let obj;
107
+ try {
108
+ obj = JSON.parse(payload);
109
+ }
110
+ catch {
111
+ continue; // a partial/truncated SSE line — skip it
112
+ }
113
+ if (obj && typeof obj === "object" && typeof obj.model === "string") {
114
+ lastModel = obj.model;
115
+ }
116
+ const b = usageFromResponseObject(obj);
117
+ if (b.present)
118
+ best = b; // keep the latest present usage (OpenAI emits it in the final chunk)
119
+ }
120
+ if (best)
121
+ return best;
122
+ return {
123
+ present: false,
124
+ ...(lastModel ? { model: lastModel } : {}),
125
+ unavailableReason: "streamed response carried no usage chunk (send stream_options.include_usage:true to get one)"
126
+ };
127
+ }
128
+ //# sourceMappingURL=openai-usage.js.map
@@ -0,0 +1,138 @@
1
+ import type { OpenAiUsageBreakdown } from "./openai-usage.js";
2
+ /** The gateway modes (run-15 implements ONLY `record`). */
3
+ export type GatewayMode = "record" | "cache" | "apply";
4
+ /** Per-axis honesty label — mirrors the cross-surface `token_source` discipline. */
5
+ export type SourceLabel = "provider-reported" | "local-estimate" | "unavailable";
6
+ /** The default local-only receipts location (gitignored — never committed, never uploaded). */
7
+ export declare const DEFAULT_GATEWAY_RECEIPTS_DIR = ".compaction/gateway";
8
+ export declare const GATEWAY_RECEIPTS_FILE = "receipts.jsonl";
9
+ /**
10
+ * The honest, content-free claim-boundary label carried on every record-mode receipt.
11
+ *
12
+ * CLAIM BOUNDARY (founder-set, run-15): a provider-backed **fresh/billed input-token reduction** MAY be
13
+ * displayed when the provider reports cached input tokens (the provider served part of the input from its
14
+ * prompt cache, so fewer input tokens were billed fresh — with model-visible bytes unchanged). What is
15
+ * NOT claimed on this path: any model-visible input reduction, any output-token reduction, or any
16
+ * cost-savings figure (cost is unavailable — the provider reports tokens, not billing).
17
+ */
18
+ export declare const GATEWAY_RECORD_LABEL: string;
19
+ /** A provider-backed fresh/billed input-token reduction, or an honest unavailable (never a zero savings). */
20
+ export interface FreshBilledInputReduction {
21
+ available: boolean;
22
+ /** Percent of input tokens the provider served from cache (= cached / prompt · 100), when available. */
23
+ pct?: number;
24
+ /** Honest basis (available) or reason (unavailable). */
25
+ note: string;
26
+ }
27
+ /**
28
+ * Compute the provider-backed fresh/billed input reduction. Available ONLY when the provider reported
29
+ * BOTH prompt input tokens (> 0) and cached input tokens (> 0): the cached portion was NOT billed as
30
+ * fresh input, so fresh/billed input is reduced by `cached / prompt`. Otherwise unavailable-with-reason —
31
+ * NEVER a zero "savings".
32
+ */
33
+ export declare function freshBilledInputReduction(usage: {
34
+ promptInputTokens?: number;
35
+ cachedInputTokens?: number;
36
+ }): FreshBilledInputReduction;
37
+ /** The compact display string per the claim boundary (allowed forms only). */
38
+ export declare function formatFreshBilledInputReduction(r: FreshBilledInputReduction): string;
39
+ export interface GatewayReceipt {
40
+ /** Content-free unique id (random UUID — never derived from content). */
41
+ receipt_id: string;
42
+ /** ISO timestamp the receipt was recorded. */
43
+ captured_at: string;
44
+ provider: string;
45
+ /** Model label the provider echoed, or "unknown" (never inferred). */
46
+ model: string;
47
+ /** The request path (e.g. `/v1/chat/completions`) — an endpoint label, not content. */
48
+ endpoint: string;
49
+ mode: GatewayMode;
50
+ /** The upstream HTTP status code. */
51
+ upstream_status: number;
52
+ /** record mode NEVER changes model-visible bytes. Always false in run-15. */
53
+ model_visible_bytes_changed: boolean;
54
+ /** Per-axis token COUNTS (only present axes ride here — never a silent zero). */
55
+ tokens: {
56
+ prompt_input?: number;
57
+ cached_input?: number;
58
+ billed_fresh_input?: number;
59
+ output?: number;
60
+ reasoning?: number;
61
+ };
62
+ /**
63
+ * Provider-backed fresh/billed input-token reduction (allowed claim): available ONLY when the provider
64
+ * reported cached input tokens; otherwise unavailable-with-reason (never a zero savings). This is the
65
+ * ONLY reduction this record-mode receipt asserts — never model-visible input, output, or cost.
66
+ */
67
+ fresh_billed_input_reduction: FreshBilledInputReduction;
68
+ /** Where the token counts came from. */
69
+ token_source: SourceLabel;
70
+ /** Where the cache-token count came from (provider-reported only when the response exposed cached_tokens). */
71
+ cache_source: SourceLabel;
72
+ /** Cost is always unavailable on this path (provider reports tokens, not billing). */
73
+ cost_source: "unavailable";
74
+ /** Honest per-axis reasons for any unavailable axis (never a bare "unavailable"). */
75
+ reasons: {
76
+ token?: string;
77
+ cache?: string;
78
+ cost: string;
79
+ };
80
+ /** Single request → run-scoped, never generalized. */
81
+ claim_scope: "run-scoped";
82
+ /** record mode applies nothing → nothing to approve. Apply/dry-run receipts carry the explicit source. */
83
+ approval_status: "not-required" | "explicit-mode" | "explicit-header" | "explicit-dry-run";
84
+ /** Local-only by default; nothing is uploaded. */
85
+ sync_status: "local-only";
86
+ content_uploaded: false;
87
+ /**
88
+ * Optional CLIENT-SET proof-run id (an opaque grouping label, NOT content) — present only when the
89
+ * client sent an `x-compaction-proof-run` header. The gateway only READS it (never injects/mutates the
90
+ * request), and it is used purely to pair receipts of a manual proof run. Content-free by construction.
91
+ */
92
+ proof_run_id?: string;
93
+ /** The honest content-free label (never upgraded to a saving/cost claim). */
94
+ label: string;
95
+ /** The deterministic policy that ran (e.g. `deterministic-dedupe`). */
96
+ policy?: string;
97
+ /** True ONLY when apply actually changed the request body (before/after evidence exists via recovery). */
98
+ request_mutated?: boolean;
99
+ /** The gateway NEVER changes the response. Always false when present. */
100
+ response_mutated?: boolean;
101
+ /** dry-run: a safe mutation was possible but NOT applied (the original was forwarded unchanged). */
102
+ candidate_available?: boolean;
103
+ /** LOCAL-ESTIMATE (chars/4) model-visible input over the safe text fields — never provider-reported. */
104
+ estimated_input_tokens_before?: number;
105
+ estimated_input_tokens_after?: number;
106
+ /** Estimated model-visible input reduction percent over the safe fields (apply/dry-run candidate). */
107
+ estimated_model_visible_input_reduction_percent?: number;
108
+ /** Where the before/after ESTIMATES came from (always local-estimate — chars/4). */
109
+ token_source_before?: SourceLabel;
110
+ /** Where the AFTER token counts came from (provider-reported when the call returned usage, else local). */
111
+ token_source_after?: SourceLabel;
112
+ /** Pointer to the locally-retained original request body (content lives in the recovery store, not here). */
113
+ recovery_id?: string;
114
+ /** Why apply did NOT change the request (fail-closed shape, conflict, or no safe duplicate found). */
115
+ fail_closed_reason?: string;
116
+ /** The honest apply claim string (only asserts a model-visible reduction when request_mutated is true). */
117
+ apply_label?: string;
118
+ }
119
+ /**
120
+ * Build ONE content-free gateway receipt from the request metadata + the provider's reported usage.
121
+ * `now`/`id` are injectable for deterministic tests. CONTENT-FREE: only `usage`'s numeric fields, the
122
+ * model label, the endpoint path, and the status ride on the receipt.
123
+ */
124
+ export declare function buildGatewayReceipt(params: {
125
+ provider: string;
126
+ endpoint: string;
127
+ mode: GatewayMode;
128
+ upstreamStatus: number;
129
+ usage: OpenAiUsageBreakdown;
130
+ /** Model from the request (content-free metadata), used only if the response did not echo one. */
131
+ requestModel?: string;
132
+ /** Optional client-set proof-run id (from an `x-compaction-proof-run` header) — an opaque grouping label. */
133
+ proofRunId?: string;
134
+ now?: () => string;
135
+ id?: () => string;
136
+ }): GatewayReceipt;
137
+ /** Append a receipt to the local-only JSONL store under `<cwd>/.compaction/gateway/`. Content-free. */
138
+ export declare function appendGatewayReceipt(receipt: GatewayReceipt, cwd?: string): Promise<string>;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Compaction Gateway RECEIPT — the content-free ground-truth record of one gateway pass (PUBLIC CLI/SDK
3
+ * code — engine-free). Charter 2026-07-04-run-15. Per the canonical architecture
4
+ * (`docs/design/compaction-gateway-architecture.md` §C.2), a receipt carries ONLY counts / structure /
5
+ * labels — never messages, completions, tool outputs, or any request/response content.
6
+ *
7
+ * Run-15 ships **record mode** only: the gateway forwards the request and response byte-for-byte and
8
+ * records this receipt. So `mode: "record"` and `model_visible_bytes_changed: false` always. There is NO
9
+ * optimization and therefore NO savings claim on this receipt — it DESCRIBES the provider's own reported
10
+ * token/cache usage honestly; it never asserts a reduction. Cost is always `unavailable` on this path
11
+ * (the provider reports tokens, not a billing figure).
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { mkdir, appendFile } from "node:fs/promises";
15
+ import path from "node:path";
16
+ /** The default local-only receipts location (gitignored — never committed, never uploaded). */
17
+ export const DEFAULT_GATEWAY_RECEIPTS_DIR = ".compaction/gateway";
18
+ export const GATEWAY_RECEIPTS_FILE = "receipts.jsonl";
19
+ /**
20
+ * The honest, content-free claim-boundary label carried on every record-mode receipt.
21
+ *
22
+ * CLAIM BOUNDARY (founder-set, run-15): a provider-backed **fresh/billed input-token reduction** MAY be
23
+ * displayed when the provider reports cached input tokens (the provider served part of the input from its
24
+ * prompt cache, so fewer input tokens were billed fresh — with model-visible bytes unchanged). What is
25
+ * NOT claimed on this path: any model-visible input reduction, any output-token reduction, or any
26
+ * cost-savings figure (cost is unavailable — the provider reports tokens, not billing).
27
+ */
28
+ export const GATEWAY_RECORD_LABEL = "record mode: request and response forwarded BYTE-FOR-BYTE (model-visible bytes unchanged). Provider-backed " +
29
+ "fresh/billed input reduction may be displayed when the provider reports cached input tokens; no model-visible " +
30
+ "input reduction, output-token reduction, or cost-savings claim is made (cost is unavailable on this path).";
31
+ /**
32
+ * Compute the provider-backed fresh/billed input reduction. Available ONLY when the provider reported
33
+ * BOTH prompt input tokens (> 0) and cached input tokens (> 0): the cached portion was NOT billed as
34
+ * fresh input, so fresh/billed input is reduced by `cached / prompt`. Otherwise unavailable-with-reason —
35
+ * NEVER a zero "savings".
36
+ */
37
+ export function freshBilledInputReduction(usage) {
38
+ const prompt = usage.promptInputTokens;
39
+ const cached = usage.cachedInputTokens;
40
+ if (prompt !== undefined && prompt > 0 && cached !== undefined && cached > 0) {
41
+ const pct = Math.round((cached / prompt) * 1000) / 10; // one decimal place
42
+ return {
43
+ available: true,
44
+ pct,
45
+ note: "provider-reported cached input tokens → fresh/billed input reduced; model-visible bytes unchanged"
46
+ };
47
+ }
48
+ return {
49
+ available: false,
50
+ note: cached === undefined || cached === 0
51
+ ? "the provider reported no cached input tokens for this request — fresh/billed input reduction unavailable"
52
+ : "the provider reported no prompt input tokens — fresh/billed input reduction unavailable"
53
+ };
54
+ }
55
+ /** The compact display string per the claim boundary (allowed forms only). */
56
+ export function formatFreshBilledInputReduction(r) {
57
+ return r.available ? `-${r.pct}% fresh/billed input` : "fresh/billed input reduction: unavailable";
58
+ }
59
+ const COST_UNAVAILABLE_REASON = "the OpenAI response reports token usage but no cost or billing figure; no cost data exists on the gateway record path";
60
+ /**
61
+ * Build ONE content-free gateway receipt from the request metadata + the provider's reported usage.
62
+ * `now`/`id` are injectable for deterministic tests. CONTENT-FREE: only `usage`'s numeric fields, the
63
+ * model label, the endpoint path, and the status ride on the receipt.
64
+ */
65
+ export function buildGatewayReceipt(params) {
66
+ const now = params.now ?? (() => new Date().toISOString());
67
+ const id = params.id ?? (() => randomUUID());
68
+ const u = params.usage;
69
+ const model = u.model ?? params.requestModel ?? "unknown";
70
+ const tokens = {
71
+ ...(u.promptInputTokens !== undefined ? { prompt_input: u.promptInputTokens } : {}),
72
+ ...(u.cachedInputTokens !== undefined ? { cached_input: u.cachedInputTokens } : {}),
73
+ ...(u.billedFreshInputTokens !== undefined ? { billed_fresh_input: u.billedFreshInputTokens } : {}),
74
+ ...(u.outputTokens !== undefined ? { output: u.outputTokens } : {}),
75
+ ...(u.reasoningTokens !== undefined ? { reasoning: u.reasoningTokens } : {})
76
+ };
77
+ const tokenSource = u.present ? "provider-reported" : "unavailable";
78
+ const cacheSource = u.cachedInputTokens !== undefined ? "provider-reported" : "unavailable";
79
+ const reduction = freshBilledInputReduction({
80
+ ...(u.promptInputTokens !== undefined ? { promptInputTokens: u.promptInputTokens } : {}),
81
+ ...(u.cachedInputTokens !== undefined ? { cachedInputTokens: u.cachedInputTokens } : {})
82
+ });
83
+ return {
84
+ receipt_id: id(),
85
+ captured_at: now(),
86
+ provider: params.provider,
87
+ model,
88
+ endpoint: params.endpoint,
89
+ mode: params.mode,
90
+ upstream_status: params.upstreamStatus,
91
+ model_visible_bytes_changed: false,
92
+ tokens,
93
+ fresh_billed_input_reduction: reduction,
94
+ token_source: tokenSource,
95
+ cache_source: cacheSource,
96
+ cost_source: "unavailable",
97
+ reasons: {
98
+ ...(u.present ? {} : { token: u.unavailableReason ?? "no usage reported by the provider" }),
99
+ ...(cacheSource === "unavailable"
100
+ ? { cache: "the provider response did not report cached input tokens (prompt_tokens_details.cached_tokens) for this request" }
101
+ : {}),
102
+ cost: COST_UNAVAILABLE_REASON
103
+ },
104
+ claim_scope: "run-scoped",
105
+ approval_status: "not-required",
106
+ sync_status: "local-only",
107
+ content_uploaded: false,
108
+ ...(params.proofRunId ? { proof_run_id: params.proofRunId } : {}),
109
+ label: GATEWAY_RECORD_LABEL
110
+ };
111
+ }
112
+ /** Append a receipt to the local-only JSONL store under `<cwd>/.compaction/gateway/`. Content-free. */
113
+ export async function appendGatewayReceipt(receipt, cwd = process.cwd()) {
114
+ const dir = path.join(cwd, DEFAULT_GATEWAY_RECEIPTS_DIR);
115
+ await mkdir(dir, { recursive: true });
116
+ const file = path.join(dir, GATEWAY_RECEIPTS_FILE);
117
+ await appendFile(file, `${JSON.stringify(receipt)}\n`, "utf8");
118
+ return file;
119
+ }
120
+ //# sourceMappingURL=receipt.js.map
@@ -0,0 +1,23 @@
1
+ export declare const GATEWAY_RECOVERY_DIR = ".compaction/gateway/recovery";
2
+ export interface RecoveryRecord {
3
+ recovery_id: string;
4
+ captured_at: string;
5
+ endpoint: string;
6
+ policy: string;
7
+ /** The EXACT original request body (pre-mutation). Local-only; the receipt never carries this. */
8
+ original_body: string;
9
+ }
10
+ /**
11
+ * Persist the original request body for recovery and return a fresh `recovery_id`. Best-effort restrictive
12
+ * permissions (0700 dir, 0600 file). Synchronous + fail-safe: throws only if the write genuinely fails
13
+ * (the caller treats a failure as "do NOT mutate" — apply must never proceed without a saved original).
14
+ */
15
+ export declare function saveOriginalForRecovery(cwd: string, params: {
16
+ endpoint: string;
17
+ policy: string;
18
+ originalBody: string;
19
+ now?: () => string;
20
+ id?: () => string;
21
+ }): string;
22
+ /** Read a recovery record back by id, or null when it does not exist / cannot be parsed. */
23
+ export declare function readRecovery(cwd: string, recoveryId: string): RecoveryRecord | null;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Compaction Gateway APPLY recovery store (PUBLIC CLI/SDK core — engine-free). Charter run-17.2.
3
+ *
4
+ * When apply mode changes a request body, the ORIGINAL body is retained LOCALLY here so the exact
5
+ * pre-mutation request is always recoverable. This is the ONE place gateway request content is persisted
6
+ * — deliberately, for recovery — and it is:
7
+ * - local-only under `<cwd>/.compaction/gateway/recovery/` (gitignored; NEVER uploaded),
8
+ * - written with restrictive `0600` permissions where the OS supports it,
9
+ * - referenced from receipts by `recovery_id` ONLY (the content-free receipt never carries the body).
10
+ *
11
+ * `compaction gateway recover <recovery_id>` reads it back. Nothing here ever leaves the machine.
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync } from "node:fs";
15
+ import path from "node:path";
16
+ export const GATEWAY_RECOVERY_DIR = ".compaction/gateway/recovery";
17
+ function recoveryDir(cwd) {
18
+ return path.join(cwd, GATEWAY_RECOVERY_DIR);
19
+ }
20
+ function recoveryPath(cwd, recoveryId) {
21
+ return path.join(recoveryDir(cwd), `${recoveryId}.json`);
22
+ }
23
+ /**
24
+ * Persist the original request body for recovery and return a fresh `recovery_id`. Best-effort restrictive
25
+ * permissions (0700 dir, 0600 file). Synchronous + fail-safe: throws only if the write genuinely fails
26
+ * (the caller treats a failure as "do NOT mutate" — apply must never proceed without a saved original).
27
+ */
28
+ export function saveOriginalForRecovery(cwd, params) {
29
+ const recoveryId = (params.id ?? (() => randomUUID()))();
30
+ const dir = recoveryDir(cwd);
31
+ mkdirSync(dir, { recursive: true });
32
+ try {
33
+ chmodSync(dir, 0o700);
34
+ }
35
+ catch {
36
+ /* best-effort — some filesystems (e.g. Windows) ignore POSIX modes */
37
+ }
38
+ const record = {
39
+ recovery_id: recoveryId,
40
+ captured_at: (params.now ?? (() => new Date().toISOString()))(),
41
+ endpoint: params.endpoint,
42
+ policy: params.policy,
43
+ original_body: params.originalBody
44
+ };
45
+ const file = recoveryPath(cwd, recoveryId);
46
+ writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
47
+ try {
48
+ chmodSync(file, 0o600);
49
+ }
50
+ catch {
51
+ /* best-effort */
52
+ }
53
+ return recoveryId;
54
+ }
55
+ /** Read a recovery record back by id, or null when it does not exist / cannot be parsed. */
56
+ export function readRecovery(cwd, recoveryId) {
57
+ const file = recoveryPath(cwd, recoveryId);
58
+ if (!existsSync(file))
59
+ return null;
60
+ try {
61
+ const rec = JSON.parse(readFileSync(file, "utf8"));
62
+ return rec && typeof rec.original_body === "string" ? rec : null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Compaction Gateway server — RECORD MODE (PUBLIC CLI/SDK code — engine-free). Charter run-15.
3
+ *
4
+ * A LOCAL, byte-safe, OpenAI-compatible reverse proxy: a developer points their OpenAI client's base URL
5
+ * at this gateway; the gateway forwards the request to the real provider BYTE-FOR-BYTE, streams the
6
+ * response back BYTE-FOR-BYTE, and records ONE content-free receipt (`receipt.ts`) with the provider's
7
+ * reported token/cache usage. It NEVER mutates the request or the response (model-visible bytes
8
+ * unchanged), NEVER stores content, and NEVER persists the client's API key.
9
+ *
10
+ * Run-15 implements ONLY `record` mode (no cache-optimization, no apply, no LCM). Built on `node:http`
11
+ * (the repo's server style — see `apps/api/src/server.ts`); zero new dependency.
12
+ */
13
+ import http from "node:http";
14
+ import type { GatewayReceipt } from "./receipt.js";
15
+ /** The server's configured gateway mode. Default `record`; `apply`/`dry-run` require explicit opt-in. */
16
+ export type GatewayServerMode = "record" | "apply" | "dry-run";
17
+ export interface GatewayServerOptions {
18
+ provider: string;
19
+ /** The upstream provider base (only its ORIGIN is used; the client's request path is authoritative). */
20
+ upstream: string;
21
+ /** `record` (default, byte-safe), or the explicit `apply`/`dry-run` deterministic modes (run-17.2). */
22
+ mode: GatewayServerMode;
23
+ /** Deterministic apply policy (run-17.2). Only `deterministic-dedupe` is implemented. */
24
+ policy?: string;
25
+ /** Where receipts are written (defaults to process.cwd()). */
26
+ cwd?: string;
27
+ /** Test/observability hook fired after each receipt is built (before/independent of the file append). */
28
+ onReceipt?: (receipt: GatewayReceipt) => void;
29
+ /** Best-effort logger for content-free operational lines (defaults to no-op). */
30
+ log?: (line: string) => void;
31
+ }
32
+ /**
33
+ * Create (but do not start) the gateway record-mode server. `createGatewayServer` is exported so tests
34
+ * can bind an ephemeral port. Every request is forwarded byte-for-byte; the receipt append is
35
+ * best-effort and NEVER alters or delays the client's response bytes.
36
+ */
37
+ export declare function createGatewayServer(options: GatewayServerOptions): http.Server;
38
+ export interface StartedGateway {
39
+ server: http.Server;
40
+ /** The bound address (host + port). */
41
+ address: {
42
+ host: string;
43
+ port: number;
44
+ };
45
+ close: () => Promise<void>;
46
+ }
47
+ /** Start the gateway on `host:port`. Resolves once it is listening. */
48
+ export declare function startGatewayServer(options: GatewayServerOptions & {
49
+ host: string;
50
+ port: number;
51
+ }): Promise<StartedGateway>;