@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,221 @@
1
+ /**
2
+ * Compaction Gateway DETERMINISTIC APPLY policy (PUBLIC CLI/SDK core — engine-free). Charter run-17.2.
3
+ *
4
+ * The FIRST real Compaction apply layer. It is **deterministic only** — NO model call, NO paraphrase, NO
5
+ * semantic summary, NO LCM. The single policy is `deterministic-dedupe`: within a known-safe text field,
6
+ * remove EXACT-duplicate LARGE blocks (preserve the first occurrence, never reorder). It fails CLOSED on
7
+ * any request shape it does not explicitly understand, and it never touches system/developer instructions,
8
+ * tool schemas, auth/config fields, or short strings.
9
+ *
10
+ * This module is PURE (no I/O): it takes the request endpoint + body text and returns a plan describing
11
+ * whether a safe mutation is possible, the mutated body (only when something actually changed), and a
12
+ * LOCAL-ESTIMATE model-visible-input reduction. The server decides what to do with the plan.
13
+ */
14
+ /** The only policy implemented in run-17.2. */
15
+ export const DEDUPE_POLICY = "deterministic-dedupe";
16
+ /** Blocks shorter than this are NEVER dedupe candidates (short strings are never touched). */
17
+ export const DEFAULT_MIN_BLOCK_CHARS = 512;
18
+ /** Chat roles we recognize. An unrecognized role → fail closed (role/order semantics uncertain). */
19
+ const KNOWN_ROLES = new Set(["system", "developer", "user", "assistant", "tool"]);
20
+ /** Only these roles' text is EVER a dedupe candidate. System/developer/assistant/tool are never touched. */
21
+ const DEDUPE_ROLES = new Set(["user"]);
22
+ /** Presence of any of these top-level fields makes the request a more complex shape → fail closed. */
23
+ const COMPLEX_FIELDS = ["tools", "functions", "tool_choice", "function_call", "response_format"];
24
+ /** chars/4 — the repo's local-estimate token convention. NEVER provider-reported. */
25
+ export function estimateTokens(chars) {
26
+ return Math.round(chars / 4);
27
+ }
28
+ /**
29
+ * Remove EXACT-duplicate large blocks from a single text field. Blocks are paragraphs split on runs of
30
+ * 2+ newlines. A block is a candidate only when it is >= `minBlockChars`; the FIRST occurrence is always
31
+ * kept, later byte-identical occurrences are dropped. Order is never changed; nothing is paraphrased.
32
+ *
33
+ * When nothing is removed the ORIGINAL string is returned unchanged (byte-exact). When blocks are removed
34
+ * the kept blocks are re-joined with a normalized `\n\n` separator (only ever in the mutated path).
35
+ */
36
+ export function dedupeTextField(text, minBlockChars) {
37
+ if (typeof text !== "string" || text.length === 0)
38
+ return { text, removed: 0 };
39
+ const parts = text.split(/(\n{2,})/); // even indices = blocks, odd = separators
40
+ const blocks = [];
41
+ for (let i = 0; i < parts.length; i += 2)
42
+ blocks.push(parts[i]);
43
+ const seen = new Set();
44
+ const kept = [];
45
+ let removed = 0;
46
+ for (const block of blocks) {
47
+ if (block.length >= minBlockChars) {
48
+ if (seen.has(block)) {
49
+ removed++;
50
+ continue; // drop later exact duplicate; first occurrence already kept
51
+ }
52
+ seen.add(block);
53
+ }
54
+ kept.push(block);
55
+ }
56
+ if (removed === 0)
57
+ return { text, removed: 0 }; // byte-exact: never mutate when nothing to remove
58
+ return { text: kept.join("\n\n"), removed };
59
+ }
60
+ function failClosed(shape, reason) {
61
+ return {
62
+ policy: DEDUPE_POLICY,
63
+ shape,
64
+ supported: false,
65
+ changed: false,
66
+ failClosedReason: reason,
67
+ removedBlocks: 0,
68
+ charsBefore: 0,
69
+ charsAfter: 0,
70
+ estTokensBefore: 0,
71
+ estTokensAfter: 0,
72
+ reductionPercent: 0
73
+ };
74
+ }
75
+ /** endpoint family from the request path (e.g. `/v1/responses`, `/responses`). */
76
+ function endpointFamily(endpoint) {
77
+ const p = endpoint.split("?")[0];
78
+ if (p.endsWith("/responses"))
79
+ return "responses";
80
+ if (p.endsWith("/chat/completions"))
81
+ return "chat";
82
+ return "other";
83
+ }
84
+ function reductionPercent(before, after) {
85
+ if (before <= 0)
86
+ return 0;
87
+ return Math.round((1 - after / before) * 1000) / 10;
88
+ }
89
+ /**
90
+ * Plan a deterministic-dedupe apply for one request. PURE — no I/O. Fails CLOSED (supported:false) on any
91
+ * shape not explicitly handled: non-JSON, unknown endpoint, multimodal/array content, tool-bearing
92
+ * requests, unknown roles, or missing safe fields. The caller forwards the ORIGINAL body whenever this
93
+ * plan is not `changed` (fail-closed or nothing-to-remove both mean byte-identical passthrough).
94
+ */
95
+ export function planDeterministicDedupe(endpoint, bodyText, opts = {}) {
96
+ const minBlockChars = opts.minBlockChars ?? DEFAULT_MIN_BLOCK_CHARS;
97
+ let obj;
98
+ try {
99
+ const parsed = JSON.parse(bodyText);
100
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
101
+ return failClosed("unsupported", "request body is not a JSON object");
102
+ }
103
+ obj = parsed;
104
+ }
105
+ catch {
106
+ return failClosed("unsupported", "request body is not valid JSON — fail closed (never mutate an unknown shape)");
107
+ }
108
+ const family = endpointFamily(endpoint);
109
+ if (family === "other") {
110
+ return failClosed("unsupported", `endpoint '${endpoint}' is not a supported apply shape (only /v1/responses and /v1/chat/completions)`);
111
+ }
112
+ // Any tool/function schema or structured-output field → a more complex shape we deliberately do not
113
+ // touch in run-17.2 (never modify tool schemas; fail closed rather than guess).
114
+ for (const f of COMPLEX_FIELDS) {
115
+ if (obj[f] !== undefined) {
116
+ return failClosed(family === "responses" ? "responses-string" : "chat-messages", `request contains '${f}' (tool/function/structured-output shape) — fail closed (never touch tool schemas)`);
117
+ }
118
+ }
119
+ if (family === "responses") {
120
+ const input = obj.input;
121
+ if (typeof input !== "string") {
122
+ return failClosed("unsupported", "responses.input is not a simple string (arrays/multimodal/other shapes fail closed)");
123
+ }
124
+ const before = input.length;
125
+ const { text: newInput, removed } = dedupeTextField(input, minBlockChars);
126
+ const after = newInput.length;
127
+ if (removed === 0) {
128
+ return {
129
+ policy: DEDUPE_POLICY,
130
+ shape: "responses-string",
131
+ supported: true,
132
+ changed: false,
133
+ removedBlocks: 0,
134
+ charsBefore: before,
135
+ charsAfter: before,
136
+ estTokensBefore: estimateTokens(before),
137
+ estTokensAfter: estimateTokens(before),
138
+ reductionPercent: 0
139
+ };
140
+ }
141
+ const mutated = { ...obj, input: newInput };
142
+ return {
143
+ policy: DEDUPE_POLICY,
144
+ shape: "responses-string",
145
+ supported: true,
146
+ changed: true,
147
+ mutatedBody: JSON.stringify(mutated),
148
+ removedBlocks: removed,
149
+ charsBefore: before,
150
+ charsAfter: after,
151
+ estTokensBefore: estimateTokens(before),
152
+ estTokensAfter: estimateTokens(after),
153
+ reductionPercent: reductionPercent(before, after)
154
+ };
155
+ }
156
+ // family === "chat": messages[].content string, user role only.
157
+ const messages = obj.messages;
158
+ if (!Array.isArray(messages) || messages.length === 0) {
159
+ return failClosed("unsupported", "chat.completions.messages is missing or not a non-empty array");
160
+ }
161
+ // Validate EVERY message first — any non-string content or unknown role → fail closed (never guess).
162
+ for (const m of messages) {
163
+ if (typeof m !== "object" || m === null || Array.isArray(m)) {
164
+ return failClosed("chat-messages", "a message is not an object — fail closed");
165
+ }
166
+ const role = m.role;
167
+ const content = m.content;
168
+ if (typeof role !== "string" || !KNOWN_ROLES.has(role)) {
169
+ return failClosed("chat-messages", `a message has an unknown/missing role — fail closed (role/order semantics uncertain)`);
170
+ }
171
+ if (typeof content !== "string") {
172
+ // multimodal content array / null / object → fail closed (never touch non-string content)
173
+ return failClosed("chat-messages", "a message has non-string content (multimodal/array) — fail closed");
174
+ }
175
+ }
176
+ let before = 0;
177
+ let after = 0;
178
+ let removedTotal = 0;
179
+ const newMessages = messages.map((m) => {
180
+ const msg = m;
181
+ if (!DEDUPE_ROLES.has(msg.role)) {
182
+ before += msg.content.length;
183
+ after += msg.content.length;
184
+ return m; // system/developer/assistant/tool text is NEVER touched
185
+ }
186
+ before += msg.content.length;
187
+ const { text: newContent, removed } = dedupeTextField(msg.content, minBlockChars);
188
+ after += newContent.length;
189
+ removedTotal += removed;
190
+ return removed === 0 ? m : { ...msg, content: newContent };
191
+ });
192
+ if (removedTotal === 0) {
193
+ return {
194
+ policy: DEDUPE_POLICY,
195
+ shape: "chat-messages",
196
+ supported: true,
197
+ changed: false,
198
+ removedBlocks: 0,
199
+ charsBefore: before,
200
+ charsAfter: before,
201
+ estTokensBefore: estimateTokens(before),
202
+ estTokensAfter: estimateTokens(before),
203
+ reductionPercent: 0
204
+ };
205
+ }
206
+ const mutated = { ...obj, messages: newMessages };
207
+ return {
208
+ policy: DEDUPE_POLICY,
209
+ shape: "chat-messages",
210
+ supported: true,
211
+ changed: true,
212
+ mutatedBody: JSON.stringify(mutated),
213
+ removedBlocks: removedTotal,
214
+ charsBefore: before,
215
+ charsAfter: after,
216
+ estTokensBefore: estimateTokens(before),
217
+ estTokensAfter: estimateTokens(after),
218
+ reductionPercent: reductionPercent(before, after)
219
+ };
220
+ }
221
+ //# sourceMappingURL=apply-policy.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Compaction Gateway APPLY receipt builder (PUBLIC CLI/SDK core — engine-free). Charter run-17.2.
3
+ *
4
+ * Builds the content-free before/after receipt for an apply / dry-run pass. It REUSES the record receipt
5
+ * builder for the provider's after-call usage (provider-reported token/cache axis) and overlays the apply
6
+ * axis: request_mutated, the LOCAL-ESTIMATE model-visible input before/after, the recovery pointer, and a
7
+ * fail_closed_reason when nothing was applied. The model-visible-input reduction is claimed ONLY when the
8
+ * request body was actually changed; otherwise no reduction is asserted. The response is NEVER changed.
9
+ */
10
+ import { type GatewayReceipt } from "./receipt.js";
11
+ import type { OpenAiUsageBreakdown } from "./openai-usage.js";
12
+ import type { DedupePlan } from "./apply-policy.js";
13
+ import type { ApplyActivation } from "./apply-activation.js";
14
+ export declare const applyLabelMutated: (pct: number) => string;
15
+ export declare const APPLY_LABEL_NOOP = "apply requested but the request was forwarded UNCHANGED (no safe duplicate to remove / fail closed). No model-visible input reduction is claimed.";
16
+ export declare const dryRunLabelCandidate: (pct: number) => string;
17
+ export declare const DRYRUN_LABEL_NONE = "dry-run (deterministic-dedupe): no safe deterministic mutation is available for this request; the original was forwarded unchanged.";
18
+ /**
19
+ * Build ONE content-free apply/dry-run receipt. `applied` is true only when the body was actually changed
20
+ * (apply path); dry-run always forwards the original (applied=false, candidate flagged from the plan).
21
+ */
22
+ export declare function buildApplyReceipt(params: {
23
+ provider: string;
24
+ endpoint: string;
25
+ upstreamStatus: number;
26
+ usage: OpenAiUsageBreakdown;
27
+ requestModel?: string;
28
+ proofRunId?: string;
29
+ activation: ApplyActivation;
30
+ plan?: DedupePlan;
31
+ applied: boolean;
32
+ recoveryId?: string;
33
+ failClosedReason?: string;
34
+ now?: () => string;
35
+ id?: () => string;
36
+ }): GatewayReceipt;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Compaction Gateway APPLY receipt builder (PUBLIC CLI/SDK core — engine-free). Charter run-17.2.
3
+ *
4
+ * Builds the content-free before/after receipt for an apply / dry-run pass. It REUSES the record receipt
5
+ * builder for the provider's after-call usage (provider-reported token/cache axis) and overlays the apply
6
+ * axis: request_mutated, the LOCAL-ESTIMATE model-visible input before/after, the recovery pointer, and a
7
+ * fail_closed_reason when nothing was applied. The model-visible-input reduction is claimed ONLY when the
8
+ * request body was actually changed; otherwise no reduction is asserted. The response is NEVER changed.
9
+ */
10
+ import { buildGatewayReceipt } from "./receipt.js";
11
+ export const applyLabelMutated = (pct) => `apply (deterministic-dedupe): model-visible input reduced by ${pct}% by a deterministic policy (exact-duplicate large blocks removed); the original request is retained locally. No output-token, cost, provider-billing, or semantic-compaction claim.`;
12
+ export const APPLY_LABEL_NOOP = "apply requested but the request was forwarded UNCHANGED (no safe duplicate to remove / fail closed). No model-visible input reduction is claimed.";
13
+ export const dryRunLabelCandidate = (pct) => `dry-run (deterministic-dedupe): a deterministic mutation is AVAILABLE (estimated -${pct}% model-visible input, local-estimate); the ORIGINAL request was forwarded unchanged. No applied-reduction claim is made.`;
14
+ export const DRYRUN_LABEL_NONE = "dry-run (deterministic-dedupe): no safe deterministic mutation is available for this request; the original was forwarded unchanged.";
15
+ /**
16
+ * Build ONE content-free apply/dry-run receipt. `applied` is true only when the body was actually changed
17
+ * (apply path); dry-run always forwards the original (applied=false, candidate flagged from the plan).
18
+ */
19
+ export function buildApplyReceipt(params) {
20
+ const base = buildGatewayReceipt({
21
+ provider: params.provider,
22
+ endpoint: params.endpoint,
23
+ mode: "apply",
24
+ upstreamStatus: params.upstreamStatus,
25
+ usage: params.usage,
26
+ ...(params.requestModel ? { requestModel: params.requestModel } : {}),
27
+ ...(params.proofRunId ? { proofRunId: params.proofRunId } : {}),
28
+ ...(params.now ? { now: params.now } : {}),
29
+ ...(params.id ? { id: params.id } : {})
30
+ });
31
+ const isDryRun = params.activation.mode === "dry-run";
32
+ const plan = params.plan;
33
+ const hasEstimate = Boolean(plan && plan.supported);
34
+ // approval_status: reflects an ACTUAL apply + its explicit source; dry-run is its own label; when nothing
35
+ // was applied nothing was approved/executed → not-required (the fail_closed_reason carries the detail).
36
+ let approval = "not-required";
37
+ if (params.applied)
38
+ approval = params.activation.activation === "explicit-header" ? "explicit-header" : "explicit-mode";
39
+ else if (isDryRun)
40
+ approval = "explicit-dry-run";
41
+ // apply label — a model-visible reduction is asserted ONLY when the body actually changed.
42
+ let applyLabel;
43
+ if (params.applied && plan)
44
+ applyLabel = applyLabelMutated(plan.reductionPercent);
45
+ else if (isDryRun)
46
+ applyLabel = plan?.changed ? dryRunLabelCandidate(plan.reductionPercent) : DRYRUN_LABEL_NONE;
47
+ else
48
+ applyLabel = APPLY_LABEL_NOOP;
49
+ const failReason = params.failClosedReason ?? plan?.failClosedReason;
50
+ return {
51
+ ...base,
52
+ // Override the inherited record label: an apply receipt must NOT claim "forwarded byte-for-byte /
53
+ // model-visible bytes unchanged" when it actually mutated. The apply label is the honest one here.
54
+ label: applyLabel,
55
+ model_visible_bytes_changed: params.applied, // TRUE only when the request body was actually changed
56
+ approval_status: approval,
57
+ policy: params.activation.policy ?? plan?.policy ?? "deterministic-dedupe",
58
+ request_mutated: params.applied,
59
+ response_mutated: false,
60
+ ...(isDryRun ? { candidate_available: Boolean(plan?.changed) } : {}),
61
+ ...(hasEstimate
62
+ ? {
63
+ estimated_input_tokens_before: plan.estTokensBefore,
64
+ estimated_input_tokens_after: params.applied ? plan.estTokensAfter : plan.estTokensBefore,
65
+ estimated_model_visible_input_reduction_percent: params.applied || isDryRun ? plan.reductionPercent : 0,
66
+ token_source_before: "local-estimate",
67
+ token_source_after: base.token_source
68
+ }
69
+ : {}),
70
+ ...(params.recoveryId ? { recovery_id: params.recoveryId } : {}),
71
+ ...(failReason ? { fail_closed_reason: failReason } : {}),
72
+ apply_label: applyLabel
73
+ };
74
+ }
75
+ //# sourceMappingURL=apply-receipt.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Compaction Gateway CACHE PROOF pairing/summary (PUBLIC CLI/SDK code — engine-free). Charter run-16.
3
+ *
4
+ * Run-16 is a PROOF run using record mode (NOT cache mode). It pairs two or more content-free gateway
5
+ * receipts from one manual proof run and surfaces the wedge:
6
+ * "Same model-visible bytes, provider-backed cached input tokens, lower fresh/billed input tokens."
7
+ *
8
+ * CONTENT-FREE by construction: it operates ONLY on receipts (which already carry counts/labels, never
9
+ * content). It computes the provider-backed fresh/billed input reduction from the provider's own reported
10
+ * cached-input accounting. It makes NO cost-savings, NO model-visible input reduction, and NO output-token
11
+ * reduction claim. When no receipt reports cached tokens → the reduction is UNAVAILABLE (never a zero
12
+ * "savings"). No proxy-side cache injection happens anywhere here — this only reads receipts.
13
+ */
14
+ import type { GatewayReceipt } from "./receipt.js";
15
+ import { type FreshBilledInputReduction } from "./receipt.js";
16
+ /** One request's content-free view for the proof table. */
17
+ export interface CacheProofRequest {
18
+ promptInput?: number;
19
+ cachedInput?: number;
20
+ /** Fresh/billed input = prompt - cached (the receipt's billed_fresh_input). */
21
+ freshInput?: number;
22
+ output?: number;
23
+ tokenSource: string;
24
+ modelVisibleBytesChanged: boolean;
25
+ }
26
+ export interface CacheProofSummary {
27
+ requests: CacheProofRequest[];
28
+ /** True only when EVERY paired receipt left model-visible bytes unchanged (record mode always does). */
29
+ modelVisibleBytesUnchanged: boolean;
30
+ /** The BEST provider-backed fresh/billed input reduction across the requests, or unavailable. */
31
+ bestReduction: FreshBilledInputReduction;
32
+ }
33
+ /** Filter receipts to one proof run (by the client-set `proof_run_id`). */
34
+ export declare function receiptsForProofRun(receipts: GatewayReceipt[], proofRunId: string): GatewayReceipt[];
35
+ /**
36
+ * Summarize a set of receipts into a cache proof. The best reduction is the largest provider-backed
37
+ * fresh/billed input reduction among the requests (typically the cached request beats the cold one).
38
+ */
39
+ export declare function summarizeCacheProof(receipts: GatewayReceipt[]): CacheProofSummary;
40
+ /**
41
+ * Render the content-free proof table (charter run-16 §B). Shows each request's provider-reported
42
+ * breakdown, then the honest claim line. When cached tokens are absent the reduction line reads
43
+ * "unavailable" with the reason — NEVER a zero "savings" presented as proof.
44
+ */
45
+ export declare function formatCacheProof(summary: CacheProofSummary): string;
@@ -0,0 +1,65 @@
1
+ import { freshBilledInputReduction } from "./receipt.js";
2
+ /** Filter receipts to one proof run (by the client-set `proof_run_id`). */
3
+ export function receiptsForProofRun(receipts, proofRunId) {
4
+ return receipts.filter((r) => r.proof_run_id === proofRunId);
5
+ }
6
+ /**
7
+ * Summarize a set of receipts into a cache proof. The best reduction is the largest provider-backed
8
+ * fresh/billed input reduction among the requests (typically the cached request beats the cold one).
9
+ */
10
+ export function summarizeCacheProof(receipts) {
11
+ const requests = receipts.map((r) => ({
12
+ ...(r.tokens.prompt_input !== undefined ? { promptInput: r.tokens.prompt_input } : {}),
13
+ ...(r.tokens.cached_input !== undefined ? { cachedInput: r.tokens.cached_input } : {}),
14
+ ...(r.tokens.billed_fresh_input !== undefined ? { freshInput: r.tokens.billed_fresh_input } : {}),
15
+ ...(r.tokens.output !== undefined ? { output: r.tokens.output } : {}),
16
+ tokenSource: r.token_source,
17
+ modelVisibleBytesChanged: r.model_visible_bytes_changed
18
+ }));
19
+ const modelVisibleBytesUnchanged = receipts.length > 0 && receipts.every((r) => r.model_visible_bytes_changed === false);
20
+ // Best = the highest available fresh/billed reduction across the receipts.
21
+ let bestReduction = {
22
+ available: false,
23
+ note: "no receipt reported provider cached input tokens — fresh/billed input reduction unavailable"
24
+ };
25
+ for (const r of receipts) {
26
+ const red = freshBilledInputReduction({
27
+ ...(r.tokens.prompt_input !== undefined ? { promptInputTokens: r.tokens.prompt_input } : {}),
28
+ ...(r.tokens.cached_input !== undefined ? { cachedInputTokens: r.tokens.cached_input } : {})
29
+ });
30
+ if (red.available && (!bestReduction.available || (red.pct ?? 0) > (bestReduction.pct ?? 0))) {
31
+ bestReduction = red;
32
+ }
33
+ }
34
+ return { requests, modelVisibleBytesUnchanged, bestReduction };
35
+ }
36
+ function n(value) {
37
+ return value === undefined ? "unavailable" : value.toLocaleString("en-US");
38
+ }
39
+ /**
40
+ * Render the content-free proof table (charter run-16 §B). Shows each request's provider-reported
41
+ * breakdown, then the honest claim line. When cached tokens are absent the reduction line reads
42
+ * "unavailable" with the reason — NEVER a zero "savings" presented as proof.
43
+ */
44
+ export function formatCacheProof(summary) {
45
+ const lines = ["COMPACTION CACHE PROOF", ""];
46
+ summary.requests.forEach((req, i) => {
47
+ lines.push(`request ${i + 1}`);
48
+ lines.push(` prompt input tokens ${n(req.promptInput).padStart(12)}`);
49
+ lines.push(` cached input tokens ${n(req.cachedInput).padStart(12)}`);
50
+ lines.push(` fresh input tokens ${n(req.freshInput).padStart(12)}`);
51
+ lines.push(` output tokens ${n(req.output).padStart(12)}`);
52
+ lines.push("");
53
+ });
54
+ lines.push(summary.modelVisibleBytesUnchanged ? "model-visible bytes unchanged" : "model-visible bytes: (mixed — check receipts)");
55
+ if (summary.bestReduction.available) {
56
+ lines.push(`fresh/billed input reduction: -${summary.bestReduction.pct}%`);
57
+ lines.push("claim: provider-backed cached-input accounting");
58
+ }
59
+ else {
60
+ lines.push("fresh/billed input reduction: unavailable");
61
+ lines.push("reason: provider did not report cached input tokens");
62
+ }
63
+ return lines.join("\n");
64
+ }
65
+ //# sourceMappingURL=cache-proof.js.map
@@ -0,0 +1,50 @@
1
+ /** The env var we set to route an OpenAI-compatible client through the gateway. */
2
+ export declare const CONFIGURE_ENV_VAR = "OPENAI_BASE_URL";
3
+ export interface ProjectDetection {
4
+ /** `.env` / `.env.local` files that exist in the project root. */
5
+ envFiles: string[];
6
+ hasPackageJson: boolean;
7
+ /** True when package.json lists the `openai` SDK as a dependency. */
8
+ usesOpenAiSdk: boolean;
9
+ /** An already-configured base URL, if any (OPENAI_BASE_URL or OPENAI_API_BASE), and where. */
10
+ existingBaseUrl?: {
11
+ file: string;
12
+ var: string;
13
+ value: string;
14
+ };
15
+ }
16
+ /** Detect the project's OpenAI-config surface WITHOUT modifying anything. */
17
+ export declare function detectProject(cwd: string): ProjectDetection;
18
+ export type ConfigureAction = "create" | "append" | "conflict";
19
+ export interface ConfigurePlan {
20
+ /** The file (relative) we would write. */
21
+ targetFile: string;
22
+ action: ConfigureAction;
23
+ /** The base URL we would set (already includes /v1). */
24
+ baseUrl: string;
25
+ /** Unified-ish diff preview (content-free — env var + gateway URL only). */
26
+ diff: string;
27
+ /** Where the `.bak` backup would go, when the target already exists. */
28
+ backupFile?: string;
29
+ detection: ProjectDetection;
30
+ /** Human-readable note about why apply is/should be gated. */
31
+ note: string;
32
+ }
33
+ /**
34
+ * Build a configure plan for pointing this project's OpenAI client at `baseUrl` (the gateway `/v1` URL).
35
+ * Never writes. If a base URL is already configured, the plan action is "conflict" and apply requires force.
36
+ */
37
+ export declare function planGatewayConfigure(cwd: string, baseUrl: string): ConfigurePlan;
38
+ export interface ApplyResult {
39
+ wrote: boolean;
40
+ targetFile: string;
41
+ backupFile?: string;
42
+ reason?: string;
43
+ }
44
+ /**
45
+ * Apply a configure plan: write a `.bak` backup (when the target exists), then create/append the base-URL
46
+ * line. Refuses a "conflict" plan unless `force` is set. This is the ONLY function here that writes.
47
+ */
48
+ export declare function applyGatewayConfigure(cwd: string, plan: ConfigurePlan, force?: boolean): ApplyResult;
49
+ /** Render a configure plan as plain content-free lines (for the CLI preview). */
50
+ export declare function formatConfigurePlan(plan: ConfigurePlan): string[];
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Gateway project configure (PUBLIC CLI core). Charter run-17.1.
3
+ *
4
+ * Approval-gated: this module only ever PLANS a change (detect the project, propose adding an
5
+ * OPENAI_BASE_URL pointing at the local gateway, and render an exact diff). It writes NOTHING unless the
6
+ * caller explicitly applies the plan, and even then it creates a `.bak` backup first and refuses to
7
+ * overwrite an existing provider base URL without an explicit force. No auto-write, ever.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ /** The env var we set to route an OpenAI-compatible client through the gateway. */
12
+ export const CONFIGURE_ENV_VAR = "OPENAI_BASE_URL";
13
+ const ENV_CANDIDATES = [".env", ".env.local"];
14
+ const BASE_URL_VARS = ["OPENAI_BASE_URL", "OPENAI_API_BASE"];
15
+ function readIfExists(p) {
16
+ try {
17
+ return fs.readFileSync(p, "utf8");
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /** Find an existing base-URL assignment in a dotenv-style body. Returns the var + value, or null. */
24
+ function findBaseUrlAssignment(body) {
25
+ for (const line of body.split(/\r?\n/)) {
26
+ const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
27
+ if (!m)
28
+ continue;
29
+ if (BASE_URL_VARS.includes(m[1])) {
30
+ return { var: m[1], value: m[2].replace(/^["']|["']$/g, "") };
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ /** Detect the project's OpenAI-config surface WITHOUT modifying anything. */
36
+ export function detectProject(cwd) {
37
+ const envFiles = [];
38
+ let existingBaseUrl;
39
+ for (const name of ENV_CANDIDATES) {
40
+ const body = readIfExists(path.join(cwd, name));
41
+ if (body === null)
42
+ continue;
43
+ envFiles.push(name);
44
+ if (!existingBaseUrl) {
45
+ const found = findBaseUrlAssignment(body);
46
+ if (found)
47
+ existingBaseUrl = { file: name, var: found.var, value: found.value };
48
+ }
49
+ }
50
+ let hasPackageJson = false;
51
+ let usesOpenAiSdk = false;
52
+ const pkgBody = readIfExists(path.join(cwd, "package.json"));
53
+ if (pkgBody !== null) {
54
+ hasPackageJson = true;
55
+ try {
56
+ const pkg = JSON.parse(pkgBody);
57
+ usesOpenAiSdk = Boolean(pkg.dependencies?.openai || pkg.devDependencies?.openai);
58
+ }
59
+ catch {
60
+ /* malformed package.json — leave usesOpenAiSdk false */
61
+ }
62
+ }
63
+ return { envFiles, hasPackageJson, usesOpenAiSdk, ...(existingBaseUrl ? { existingBaseUrl } : {}) };
64
+ }
65
+ /**
66
+ * Build a configure plan for pointing this project's OpenAI client at `baseUrl` (the gateway `/v1` URL).
67
+ * Never writes. If a base URL is already configured, the plan action is "conflict" and apply requires force.
68
+ */
69
+ export function planGatewayConfigure(cwd, baseUrl) {
70
+ const detection = detectProject(cwd);
71
+ const targetFile = detection.envFiles.includes(".env.local")
72
+ ? ".env.local"
73
+ : detection.envFiles.includes(".env")
74
+ ? ".env"
75
+ : ".env";
76
+ const targetPath = path.join(cwd, targetFile);
77
+ const existing = readIfExists(targetPath);
78
+ const newLine = `${CONFIGURE_ENV_VAR}=${baseUrl}`;
79
+ let action;
80
+ let diff;
81
+ let note;
82
+ if (detection.existingBaseUrl) {
83
+ action = "conflict";
84
+ diff =
85
+ `- ${detection.existingBaseUrl.var}=${detection.existingBaseUrl.value} (in ${detection.existingBaseUrl.file})\n` +
86
+ `+ ${newLine}`;
87
+ note =
88
+ `${detection.existingBaseUrl.file} already sets ${detection.existingBaseUrl.var}. ` +
89
+ "Compaction will NOT overwrite an existing provider base URL without explicit confirmation (--force).";
90
+ }
91
+ else if (existing === null) {
92
+ action = "create";
93
+ diff = `+ ${newLine}`;
94
+ note = `${targetFile} does not exist yet. Apply would create it with just the gateway base URL line.`;
95
+ }
96
+ else {
97
+ action = "append";
98
+ diff = ` (existing ${targetFile} unchanged)\n+ ${newLine}`;
99
+ note = `Apply would append one line to ${targetFile} (a .bak backup is written first).`;
100
+ }
101
+ return {
102
+ targetFile,
103
+ action,
104
+ baseUrl,
105
+ diff,
106
+ ...(existing !== null ? { backupFile: `${targetFile}.bak` } : {}),
107
+ detection,
108
+ note
109
+ };
110
+ }
111
+ /**
112
+ * Apply a configure plan: write a `.bak` backup (when the target exists), then create/append the base-URL
113
+ * line. Refuses a "conflict" plan unless `force` is set. This is the ONLY function here that writes.
114
+ */
115
+ export function applyGatewayConfigure(cwd, plan, force = false) {
116
+ if (plan.action === "conflict" && !force) {
117
+ return { wrote: false, targetFile: plan.targetFile, reason: "existing provider base URL present; re-run with --force to overwrite" };
118
+ }
119
+ const targetPath = path.join(cwd, plan.targetFile);
120
+ const existing = readIfExists(targetPath);
121
+ const newLine = `${CONFIGURE_ENV_VAR}=${plan.baseUrl}`;
122
+ let backupFile;
123
+ if (existing !== null) {
124
+ backupFile = `${plan.targetFile}.bak`;
125
+ fs.writeFileSync(path.join(cwd, backupFile), existing, "utf8");
126
+ }
127
+ let next;
128
+ if (plan.action === "conflict" && existing !== null) {
129
+ // Replace the existing base-URL assignment line in place.
130
+ next = existing
131
+ .split(/\r?\n/)
132
+ .map((line) => {
133
+ const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=/);
134
+ return m && BASE_URL_VARS.includes(m[1]) ? newLine : line;
135
+ })
136
+ .join("\n");
137
+ }
138
+ else if (existing !== null) {
139
+ next = existing.endsWith("\n") ? `${existing}${newLine}\n` : `${existing}\n${newLine}\n`;
140
+ }
141
+ else {
142
+ next = `${newLine}\n`;
143
+ }
144
+ fs.writeFileSync(targetPath, next, "utf8");
145
+ return { wrote: true, targetFile: plan.targetFile, ...(backupFile ? { backupFile } : {}) };
146
+ }
147
+ /** Render a configure plan as plain content-free lines (for the CLI preview). */
148
+ export function formatConfigurePlan(plan) {
149
+ const lines = [];
150
+ lines.push("compaction gateway configure — proposed change (nothing is written yet)");
151
+ lines.push(` target file: ${plan.targetFile} (${plan.action})`);
152
+ lines.push(` detected: ${describeDetection(plan.detection)}`);
153
+ lines.push(" diff:");
154
+ for (const d of plan.diff.split("\n"))
155
+ lines.push(` ${d}`);
156
+ lines.push(` note: ${plan.note}`);
157
+ lines.push("");
158
+ lines.push(" to apply: compaction gateway configure --apply" + (plan.action === "conflict" ? " --force" : ""));
159
+ return lines;
160
+ }
161
+ function describeDetection(d) {
162
+ const bits = [];
163
+ bits.push(d.envFiles.length ? `env files: ${d.envFiles.join(", ")}` : "no .env files");
164
+ bits.push(d.hasPackageJson ? (d.usesOpenAiSdk ? "openai SDK in package.json" : "package.json (no openai dep)") : "no package.json");
165
+ if (d.existingBaseUrl)
166
+ bits.push(`existing ${d.existingBaseUrl.var} in ${d.existingBaseUrl.file}`);
167
+ return bits.join("; ");
168
+ }
169
+ //# sourceMappingURL=configure.js.map