@compaction/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +142 -64
  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 +9 -5
  10. package/dist/cli/commands/dev.d.ts +17 -0
  11. package/dist/cli/commands/dev.js +131 -0
  12. package/dist/cli/commands/gateway.d.ts +31 -0
  13. package/dist/cli/commands/gateway.js +312 -0
  14. package/dist/cli/commands/hooks.d.ts +5 -0
  15. package/dist/cli/commands/hooks.js +181 -0
  16. package/dist/cli/commands/init.js +422 -17
  17. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  18. package/dist/cli/commands/input-compaction-ab.js +125 -0
  19. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  20. package/dist/cli/commands/optimize-hosted.js +123 -0
  21. package/dist/cli/commands/optimize.js +8 -0
  22. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  23. package/dist/cli/commands/output-shaping-ab.js +132 -0
  24. package/dist/cli/commands/output-shaping.d.ts +7 -0
  25. package/dist/cli/commands/output-shaping.js +43 -0
  26. package/dist/cli/commands/policies.d.ts +2 -0
  27. package/dist/cli/commands/policies.js +80 -0
  28. package/dist/cli/commands/precall.d.ts +19 -0
  29. package/dist/cli/commands/precall.js +436 -0
  30. package/dist/cli/commands/recommend.js +1 -1
  31. package/dist/cli/commands/run.js +297 -12
  32. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  33. package/dist/cli/commands/upgrade-status.js +152 -0
  34. package/dist/cli/cursor-export-read.d.ts +13 -0
  35. package/dist/cli/cursor-export-read.js +53 -0
  36. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  37. package/dist/cli/cursor-live-preflight.js +46 -0
  38. package/dist/cli/index.js +36 -2
  39. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  40. package/dist/cli/onboarding/GatewayTui.js +113 -0
  41. package/dist/cli/onboarding/model.d.ts +32 -1
  42. package/dist/cli/onboarding/model.js +27 -2
  43. package/dist/cli/terminal-logo.d.ts +27 -0
  44. package/dist/cli/terminal-logo.js +49 -0
  45. package/dist/core/activity-event.d.ts +107 -0
  46. package/dist/core/activity-event.js +182 -0
  47. package/dist/core/activity-store.d.ts +63 -0
  48. package/dist/core/activity-store.js +254 -0
  49. package/dist/core/activity-view.d.ts +76 -0
  50. package/dist/core/activity-view.js +120 -0
  51. package/dist/core/api-client/index.d.ts +3 -1
  52. package/dist/core/api-client/index.js +4 -0
  53. package/dist/core/api-client/payload.d.ts +5 -1
  54. package/dist/core/api-client/payload.js +8 -6
  55. package/dist/core/api-client/persisted-config.d.ts +57 -0
  56. package/dist/core/api-client/persisted-config.js +156 -0
  57. package/dist/core/api-client/tool.d.ts +29 -0
  58. package/dist/core/api-client/tool.js +50 -0
  59. package/dist/core/api-client/types.d.ts +15 -4
  60. package/dist/core/auto-apply-ask.d.ts +58 -0
  61. package/dist/core/auto-apply-ask.js +105 -0
  62. package/dist/core/auto-apply-gates.d.ts +76 -0
  63. package/dist/core/auto-apply-gates.js +113 -0
  64. package/dist/core/before-call-activity.d.ts +35 -0
  65. package/dist/core/before-call-activity.js +103 -0
  66. package/dist/core/before-call-recovery.d.ts +19 -0
  67. package/dist/core/before-call-recovery.js +46 -0
  68. package/dist/core/before-call-stdin.d.ts +59 -0
  69. package/dist/core/before-call-stdin.js +78 -0
  70. package/dist/core/before-call.d.ts +149 -0
  71. package/dist/core/before-call.js +358 -0
  72. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  73. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  74. package/dist/core/capture-record.d.ts +40 -0
  75. package/dist/core/capture-record.js +55 -0
  76. package/dist/core/claude-code-before-call.d.ts +37 -0
  77. package/dist/core/claude-code-before-call.js +120 -0
  78. package/dist/core/claude-code-connect.d.ts +31 -0
  79. package/dist/core/claude-code-connect.js +87 -0
  80. package/dist/core/claude-code-hook-record.d.ts +71 -0
  81. package/dist/core/claude-code-hook-record.js +0 -0
  82. package/dist/core/claude-code-hooks.d.ts +77 -0
  83. package/dist/core/claude-code-hooks.js +141 -0
  84. package/dist/core/codex-capture.d.ts +45 -0
  85. package/dist/core/codex-capture.js +204 -0
  86. package/dist/core/command-runner.js +5 -1
  87. package/dist/core/compaction-artifacts.d.ts +51 -0
  88. package/dist/core/compaction-artifacts.js +242 -0
  89. package/dist/core/compactor.d.ts +9 -0
  90. package/dist/core/compactor.js +95 -0
  91. package/dist/core/cross-surface-event.d.ts +306 -0
  92. package/dist/core/cross-surface-event.js +330 -0
  93. package/dist/core/cursor-capture.d.ts +54 -0
  94. package/dist/core/cursor-capture.js +215 -0
  95. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  96. package/dist/core/cursor-preflight-probe.js +88 -0
  97. package/dist/core/cursor-preflight.d.ts +86 -0
  98. package/dist/core/cursor-preflight.js +126 -0
  99. package/dist/core/gateway/apply-activation.d.ts +39 -0
  100. package/dist/core/gateway/apply-activation.js +84 -0
  101. package/dist/core/gateway/apply-policy.d.ts +64 -0
  102. package/dist/core/gateway/apply-policy.js +221 -0
  103. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  104. package/dist/core/gateway/apply-receipt.js +75 -0
  105. package/dist/core/gateway/cache-proof.d.ts +45 -0
  106. package/dist/core/gateway/cache-proof.js +65 -0
  107. package/dist/core/gateway/configure.d.ts +50 -0
  108. package/dist/core/gateway/configure.js +169 -0
  109. package/dist/core/gateway/openai-usage.d.ts +56 -0
  110. package/dist/core/gateway/openai-usage.js +128 -0
  111. package/dist/core/gateway/receipt.d.ts +138 -0
  112. package/dist/core/gateway/receipt.js +120 -0
  113. package/dist/core/gateway/recovery.d.ts +23 -0
  114. package/dist/core/gateway/recovery.js +68 -0
  115. package/dist/core/gateway/server.d.ts +51 -0
  116. package/dist/core/gateway/server.js +276 -0
  117. package/dist/core/gateway/status.d.ts +45 -0
  118. package/dist/core/gateway/status.js +109 -0
  119. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  120. package/dist/core/hook-usage-aggregate.js +161 -0
  121. package/dist/core/input-compaction-ab.d.ts +111 -0
  122. package/dist/core/input-compaction-ab.js +158 -0
  123. package/dist/core/local-run-record.d.ts +109 -0
  124. package/dist/core/local-run-record.js +223 -0
  125. package/dist/core/output-shaping-ab.d.ts +140 -0
  126. package/dist/core/output-shaping-ab.js +146 -0
  127. package/dist/core/output-shaping-attach.d.ts +31 -0
  128. package/dist/core/output-shaping-attach.js +57 -0
  129. package/dist/core/output-shaping.d.ts +56 -0
  130. package/dist/core/output-shaping.js +89 -0
  131. package/dist/core/policy-middleware.d.ts +121 -0
  132. package/dist/core/policy-middleware.js +919 -0
  133. package/dist/core/policy-preferences.d.ts +99 -0
  134. package/dist/core/policy-preferences.js +232 -0
  135. package/dist/core/run-aggregator.d.ts +11 -1
  136. package/dist/core/run-aggregator.js +29 -2
  137. package/dist/core/run-flow-report.d.ts +82 -0
  138. package/dist/core/run-flow-report.js +71 -0
  139. package/dist/core/safety-report.js +8 -1
  140. package/dist/core/shim-capture-bridge.d.ts +32 -0
  141. package/dist/core/shim-capture-bridge.js +88 -0
  142. package/dist/core/skill-injection-policy.d.ts +72 -0
  143. package/dist/core/skill-injection-policy.js +183 -0
  144. package/dist/core/spend-attribution.js +1 -1
  145. package/dist/core/token-accounting.d.ts +1 -1
  146. package/dist/core/tool-shim.d.ts +129 -0
  147. package/dist/core/tool-shim.js +447 -0
  148. package/dist/core/trace-parser.d.ts +13 -13
  149. package/dist/core/trace-parser.js +6 -6
  150. package/dist/core/types.d.ts +12 -2
  151. package/dist/core/waste-detector.d.ts +20 -0
  152. package/dist/core/waste-detector.js +160 -6
  153. package/package.json +1 -1
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Compaction Gateway APPLY activation resolver (PUBLIC CLI/SDK core — engine-free). Charter run-17.2.
3
+ *
4
+ * Apply mode is OPT-IN and EXPLICIT. It activates only through explicit user intent — the gateway's
5
+ * `--mode apply --policy deterministic-dedupe` OR the per-request headers `x-compaction-mode: apply` +
6
+ * `x-compaction-policy: deterministic-dedupe`. When the server's configured intent and the request
7
+ * headers CONFLICT, this resolver FAILS CLOSED (falls back to record, records the reason). Default is
8
+ * always record (byte-safe, no mutation).
9
+ */
10
+ import { DEDUPE_POLICY } from "./apply-policy.js";
11
+ /** Normalize a mode string to a known effective mode, or undefined when unset/unknown. */
12
+ function normalizeMode(v) {
13
+ if (v === undefined)
14
+ return undefined;
15
+ const s = v.trim().toLowerCase();
16
+ if (s === "")
17
+ return undefined;
18
+ if (s === "record" || s === "apply" || s === "dry-run")
19
+ return s;
20
+ return "unknown";
21
+ }
22
+ /** A mutating intent is apply or dry-run; record is the non-mutating default. */
23
+ function isMutating(m) {
24
+ return m === "apply" || m === "dry-run";
25
+ }
26
+ function record() {
27
+ return { mode: "record", requested: false, activation: "record" };
28
+ }
29
+ function failClosed(reason) {
30
+ return { mode: "record", requested: true, activation: "fail-closed", failClosedReason: reason };
31
+ }
32
+ /**
33
+ * Resolve the effective apply activation from the server config + request headers. PURE.
34
+ *
35
+ * Precedence + conflict rules (fail closed on any disagreement):
36
+ * - Default server mode `record` carries no mutating intent → a header alone may activate apply/dry-run.
37
+ * - If BOTH the server AND a header express a mutating intent and they DIFFER (mode or policy) → conflict.
38
+ * - An explicit header `record` is an opt-OUT → record (never a conflict).
39
+ * - Apply/dry-run requires the known policy `deterministic-dedupe`; anything else fails closed.
40
+ * - An unknown mode value fails closed.
41
+ */
42
+ export function resolveApplyActivation(params) {
43
+ const serverMode = normalizeMode(params.serverMode);
44
+ const headerMode = normalizeMode(params.headerMode);
45
+ if (serverMode === "unknown")
46
+ return failClosed(`unknown gateway --mode '${params.serverMode}'`);
47
+ if (headerMode === "unknown")
48
+ return failClosed(`unknown x-compaction-mode header '${params.headerMode}'`);
49
+ // Explicit header opt-out → record, no matter the server mode.
50
+ if (headerMode === "record")
51
+ return record();
52
+ const serverIntent = isMutating(serverMode) ? serverMode : undefined; // record server → no intent
53
+ const headerIntent = isMutating(headerMode) ? headerMode : undefined;
54
+ // No mutating intent anywhere → plain record.
55
+ if (!serverIntent && !headerIntent)
56
+ return record();
57
+ const serverPolicy = params.serverPolicy?.trim() || undefined;
58
+ const headerPolicy = params.headerPolicy?.trim() || undefined;
59
+ // Conflict: both sides express a mutating intent and disagree on mode or policy.
60
+ if (serverIntent && headerIntent) {
61
+ if (serverIntent !== headerIntent) {
62
+ return failClosed(`gateway --mode '${serverIntent}' conflicts with x-compaction-mode '${headerIntent}' — fail closed`);
63
+ }
64
+ if (serverPolicy && headerPolicy && serverPolicy !== headerPolicy) {
65
+ return failClosed(`gateway --policy '${serverPolicy}' conflicts with x-compaction-policy '${headerPolicy}' — fail closed`);
66
+ }
67
+ }
68
+ const mode = (headerIntent ?? serverIntent);
69
+ const policy = headerPolicy ?? serverPolicy;
70
+ if (policy !== DEDUPE_POLICY) {
71
+ return failClosed(policy === undefined
72
+ ? `${mode} requested without a policy — apply requires policy '${DEDUPE_POLICY}'`
73
+ : `unknown policy '${policy}' — apply requires policy '${DEDUPE_POLICY}'`);
74
+ }
75
+ const bySource = mode === "apply"
76
+ ? headerIntent
77
+ ? "explicit-header"
78
+ : "explicit-mode"
79
+ : headerIntent
80
+ ? "dry-run-header"
81
+ : "dry-run-mode";
82
+ return { mode, requested: true, policy: DEDUPE_POLICY, activation: bySource };
83
+ }
84
+ //# sourceMappingURL=apply-activation.js.map
@@ -0,0 +1,64 @@
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 declare const DEDUPE_POLICY = "deterministic-dedupe";
16
+ export type ApplyPolicyName = typeof DEDUPE_POLICY;
17
+ /** Blocks shorter than this are NEVER dedupe candidates (short strings are never touched). */
18
+ export declare const DEFAULT_MIN_BLOCK_CHARS = 512;
19
+ export type RequestShape = "responses-string" | "chat-messages" | "unsupported";
20
+ export interface DedupePlan {
21
+ policy: ApplyPolicyName;
22
+ shape: RequestShape;
23
+ /** The shape is one we handle. */
24
+ supported: boolean;
25
+ /** A safe mutation was actually produced (duplicates removed). Only then is `mutatedBody` set. */
26
+ changed: boolean;
27
+ /** Why we will NOT apply (present whenever `supported` is false, or parsing failed). */
28
+ failClosedReason?: string;
29
+ /** Serialized JSON of the mutated request — present ONLY when `changed` is true. */
30
+ mutatedBody?: string;
31
+ /** How many exact-duplicate large blocks were removed. */
32
+ removedBlocks: number;
33
+ /** Model-visible input size over the SAFE text fields only (chars) — the honest before/after basis. */
34
+ charsBefore: number;
35
+ charsAfter: number;
36
+ /** LOCAL-ESTIMATE token counts (chars/4) — never provider-reported, never a billing figure. */
37
+ estTokensBefore: number;
38
+ estTokensAfter: number;
39
+ /** Estimated model-visible input reduction percent over the safe fields (0 when nothing changed). */
40
+ reductionPercent: number;
41
+ }
42
+ /** chars/4 — the repo's local-estimate token convention. NEVER provider-reported. */
43
+ export declare function estimateTokens(chars: number): number;
44
+ /**
45
+ * Remove EXACT-duplicate large blocks from a single text field. Blocks are paragraphs split on runs of
46
+ * 2+ newlines. A block is a candidate only when it is >= `minBlockChars`; the FIRST occurrence is always
47
+ * kept, later byte-identical occurrences are dropped. Order is never changed; nothing is paraphrased.
48
+ *
49
+ * When nothing is removed the ORIGINAL string is returned unchanged (byte-exact). When blocks are removed
50
+ * the kept blocks are re-joined with a normalized `\n\n` separator (only ever in the mutated path).
51
+ */
52
+ export declare function dedupeTextField(text: string, minBlockChars: number): {
53
+ text: string;
54
+ removed: number;
55
+ };
56
+ /**
57
+ * Plan a deterministic-dedupe apply for one request. PURE — no I/O. Fails CLOSED (supported:false) on any
58
+ * shape not explicitly handled: non-JSON, unknown endpoint, multimodal/array content, tool-bearing
59
+ * requests, unknown roles, or missing safe fields. The caller forwards the ORIGINAL body whenever this
60
+ * plan is not `changed` (fail-closed or nothing-to-remove both mean byte-identical passthrough).
61
+ */
62
+ export declare function planDeterministicDedupe(endpoint: string, bodyText: string, opts?: {
63
+ minBlockChars?: number;
64
+ }): DedupePlan;
@@ -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[];