@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,330 @@
1
+ /** Contract §2: which Compaction surface produced the event (exact values, verbatim). */
2
+ export const CROSS_SURFACE_SURFACES = [
3
+ "cli",
4
+ "claude_code",
5
+ "codex",
6
+ "cursor",
7
+ "browser_extension",
8
+ "api_metered"
9
+ ];
10
+ /** Contract §2: whose model/service was used — distinct from `surface`. */
11
+ export const CROSS_SURFACE_PROVIDERS = ["anthropic", "openai", "cursor", "chatgpt", "other"];
12
+ /** Contract §2: per-axis token source. Input and output are INDEPENDENT axes. */
13
+ export const CROSS_SURFACE_TOKEN_SOURCES = ["provider-reported", "local-estimate", "unavailable"];
14
+ /** Contract §2: how the cost figure (if any) is known. `operator-entered` is the billing-delta operator path. */
15
+ export const CROSS_SURFACE_COST_SOURCES = [
16
+ "provider-reported",
17
+ "operator-entered",
18
+ "local-estimate",
19
+ "unavailable"
20
+ ];
21
+ /**
22
+ * Contract §2 honesty rule: what a figure may be scoped to — this run / this workflow / these
23
+ * conditions. A GENERALIZED claim is intentionally NOT representable (D6: "Generalized
24
+ * billing-confirmed = future only. Public generalized 'saves X%' is forbidden.").
25
+ * `billing-confirmed-workflow-scoped` exists only on the `BillingConfirmedCrossSurfaceEvent` arm.
26
+ */
27
+ export const CROSS_SURFACE_CLAIM_SCOPES = [
28
+ "run-scoped",
29
+ "workflow-scoped",
30
+ "conditions-scoped",
31
+ "billing-confirmed-workflow-scoped"
32
+ ];
33
+ /* ------------------------------------------------------------------------------------------------
34
+ * Surfaces ↔ existing wire ToolName mapping (additive helper; changes neither enum).
35
+ * ---------------------------------------------------------------------------------------------- */
36
+ /**
37
+ * Map the EXISTING wire `ToolName` to the contract `surface` value, honestly:
38
+ * - `claude-code` → `claude_code`, `codex` → `codex`, `cursor` → `cursor`;
39
+ * - `other` (the bare `compaction run -- <cmd>` wrapper) → `cli` per the tier table ("bare
40
+ * run/trace analysis without provider usage");
41
+ * - `openai-agents` → `undefined`: the contract names no surface for it, and unknown stays
42
+ * unknown — this helper never guesses (named residual, like the control-plane enum alignment).
43
+ */
44
+ export function surfaceForToolName(tool) {
45
+ switch (tool) {
46
+ case "claude-code":
47
+ return "claude_code";
48
+ case "codex":
49
+ return "codex";
50
+ case "cursor":
51
+ return "cursor";
52
+ case "other":
53
+ return "cli";
54
+ case "openai-agents":
55
+ return undefined;
56
+ }
57
+ }
58
+ /* ------------------------------------------------------------------------------------------------
59
+ * REPORT-ONLY validator. Returns problems; never throws. It exists for the states TypeScript
60
+ * cannot police (parsed JSON, widened objects, wire data) and encodes the contract's honesty
61
+ * rules as label-correctness checks.
62
+ * ---------------------------------------------------------------------------------------------- */
63
+ /**
64
+ * Contract tier table (§3, D3/D4): surfaces whose token tier can NEVER be provider-reported —
65
+ * Cursor emits no usage (vendor gap; "never provider-reported"), and the browser's visible
66
+ * surface exposes no provider usage ("may NOT claim provider-reported web tokens").
67
+ */
68
+ export const NEVER_PROVIDER_REPORTED_SURFACES = ["cursor", "browser_extension"];
69
+ function isPlainObject(value) {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
72
+ function isOneOf(value, allowed) {
73
+ return typeof value === "string" && allowed.includes(value);
74
+ }
75
+ function isNonEmptyString(value) {
76
+ return typeof value === "string" && value.trim() !== "";
77
+ }
78
+ /** A count field may be absent, `null` (explicitly not counted), or a finite number ≥ 0. */
79
+ function countProblem(field, value) {
80
+ if (value === undefined || value === null)
81
+ return undefined;
82
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0)
83
+ return undefined;
84
+ return `${field}: must be a finite number >= 0, null, or absent`;
85
+ }
86
+ const INPUT_COUNT_FIELDS = ["input_before", "input_after"];
87
+ const OUTPUT_COUNT_FIELDS = ["output_before", "output_after", "output_estimate"];
88
+ function validateAxis(axisName, axis, event, problems) {
89
+ const countFields = axisName === "input" ? INPUT_COUNT_FIELDS : OUTPUT_COUNT_FIELDS;
90
+ if (!isPlainObject(axis)) {
91
+ problems.push(`token_source.${axisName}: must be an object with a per-axis source`);
92
+ return;
93
+ }
94
+ if (!isOneOf(axis.source, CROSS_SURFACE_TOKEN_SOURCES)) {
95
+ problems.push(`token_source.${axisName}.source: must be one of ${CROSS_SURFACE_TOKEN_SOURCES.join(" | ")} (exact)`);
96
+ return;
97
+ }
98
+ if (axis.source === "unavailable") {
99
+ // Contract §2: field-level unavailable WITH the exact reason — required per axis.
100
+ if (!isNonEmptyString(axis.unavailable_reason)) {
101
+ problems.push(`token_source.${axisName}.unavailable_reason: required when the ${axisName} axis is unavailable (the exact honest reason)`);
102
+ }
103
+ // Contract §2: never a silent zero — an unavailable axis never carries a numeric count
104
+ // (a 0 here would dress a missing count as a real one).
105
+ for (const field of countFields) {
106
+ if (typeof event[field] === "number") {
107
+ problems.push(`${field}: no silent zero — the ${axisName} axis is unavailable, so a numeric count (including 0) must not be present`);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Validate one parsed/unknown cross-surface event against the contract. REPORT-ONLY: returns the
114
+ * list of problems (empty = contract-honest) and NEVER throws — mirroring the repo's
115
+ * skipped-with-reason validator style. Rules enforced here are exactly the ones the type system
116
+ * cannot enforce on unknown data:
117
+ *
118
+ * 1. exact enums for surface / provider / token sources / cost source / claim scope;
119
+ * 2. D5 hard guardrail: a fixed-plan or plan-efficiency event is NEVER billing-confirmed;
120
+ * 3. D6: billing-confirmed requires surface api_metered + provider-reported|operator-entered cost;
121
+ * 4. tier table (D3/D4): cursor and browser_extension events never carry provider-reported tokens;
122
+ * 5. no silent zero: an unavailable axis never carries a numeric count;
123
+ * 6. per-axis unavailable_reason required when an axis is unavailable;
124
+ * 7. a numeric count requires its axis to be labeled (a count without a token_source is unlabeled).
125
+ */
126
+ export function validateCrossSurfaceEvent(value) {
127
+ const problems = [];
128
+ if (!isPlainObject(value)) {
129
+ return { problems: ["event: must be a JSON object"] };
130
+ }
131
+ const event = value;
132
+ // Rule 1 — exact enums.
133
+ if (!isOneOf(event.surface, CROSS_SURFACE_SURFACES)) {
134
+ problems.push(`surface: must be one of ${CROSS_SURFACE_SURFACES.join(" | ")} (exact)`);
135
+ }
136
+ if (event.provider !== undefined && !isOneOf(event.provider, CROSS_SURFACE_PROVIDERS)) {
137
+ problems.push(`provider: must be one of ${CROSS_SURFACE_PROVIDERS.join(" | ")} (exact)`);
138
+ }
139
+ if (event.cost_source !== undefined && !isOneOf(event.cost_source, CROSS_SURFACE_COST_SOURCES)) {
140
+ problems.push(`cost_source: must be one of ${CROSS_SURFACE_COST_SOURCES.join(" | ")} (exact)`);
141
+ }
142
+ if (event.claim_scope !== undefined && !isOneOf(event.claim_scope, CROSS_SURFACE_CLAIM_SCOPES)) {
143
+ problems.push(`claim_scope: must be one of ${CROSS_SURFACE_CLAIM_SCOPES.join(" | ")} (exact; generalized claims are not representable)`);
144
+ }
145
+ if (event.cost_source === "unavailable" && !isNonEmptyString(event.cost_unavailable_reason)) {
146
+ problems.push("cost_unavailable_reason: required when cost_source is unavailable (the exact honest reason)");
147
+ }
148
+ // Count fields are well-formed (absent | null | finite >= 0).
149
+ for (const field of [...INPUT_COUNT_FIELDS, ...OUTPUT_COUNT_FIELDS]) {
150
+ const problem = countProblem(field, event[field]);
151
+ if (problem !== undefined)
152
+ problems.push(problem);
153
+ }
154
+ // Rules 5–6 — per-axis honesty; rule 7 — counts require a labeled axis.
155
+ const tokenSource = event.token_source;
156
+ if (tokenSource !== undefined) {
157
+ if (!isPlainObject(tokenSource)) {
158
+ problems.push("token_source: must be an object with input and output axes");
159
+ }
160
+ else {
161
+ validateAxis("input", tokenSource.input, event, problems);
162
+ validateAxis("output", tokenSource.output, event, problems);
163
+ }
164
+ }
165
+ else {
166
+ for (const field of [...INPUT_COUNT_FIELDS, ...OUTPUT_COUNT_FIELDS]) {
167
+ if (typeof event[field] === "number") {
168
+ problems.push(`${field}: a numeric count requires token_source for its axis — an unlabeled count is not honest`);
169
+ }
170
+ }
171
+ }
172
+ // Rule 4 — tier ceilings (D3/D4): cursor + browser events never carry provider-reported tokens.
173
+ if (isOneOf(event.surface, NEVER_PROVIDER_REPORTED_SURFACES) &&
174
+ isPlainObject(tokenSource)) {
175
+ for (const axisName of ["input", "output"]) {
176
+ const axis = tokenSource[axisName];
177
+ if (isPlainObject(axis) && axis.source === "provider-reported") {
178
+ problems.push(`token_source.${axisName}.source: surface "${String(event.surface)}" can never be provider-reported (contract tier table — vendor exposes no usage)`);
179
+ }
180
+ }
181
+ }
182
+ // Rules 2–3 — the D5/D6 guardrail on unknown data.
183
+ const planEfficiency = event.plan_efficiency;
184
+ if (planEfficiency !== undefined) {
185
+ if (!isPlainObject(planEfficiency)) {
186
+ problems.push("plan_efficiency: must be an object");
187
+ }
188
+ else {
189
+ if (planEfficiency.evidence_type !== "plan-efficiency") {
190
+ problems.push('plan_efficiency.evidence_type: must be the literal "plan-efficiency"');
191
+ }
192
+ if (planEfficiency.billing_confirmed !== false) {
193
+ problems.push("plan_efficiency.billing_confirmed: must be the literal false — a fixed-plan figure can NEVER be billing-confirmed (contract §4, D5)");
194
+ }
195
+ }
196
+ }
197
+ const claimsBillingConfirmed = event.claim_scope === "billing-confirmed-workflow-scoped";
198
+ const isFixedPlanEvent = event.billing_model === "fixed-plan" ||
199
+ planEfficiency !== undefined ||
200
+ isOneOf(event.surface, NEVER_PROVIDER_REPORTED_SURFACES);
201
+ if (claimsBillingConfirmed) {
202
+ if (event.surface !== "api_metered") {
203
+ problems.push(`claim_scope: billing-confirmed is only possible on surface "api_metered" (contract §5, D6) — surface "${String(event.surface)}" can never be billing-confirmed`);
204
+ }
205
+ if (event.cost_source !== "provider-reported" && event.cost_source !== "operator-entered") {
206
+ problems.push("cost_source: a billing-confirmed claim requires provider-reported or operator-entered cost (contract §5, D6) — never a price-table/local estimate relabeled");
207
+ }
208
+ if (event.billing_model === "fixed-plan" || planEfficiency !== undefined) {
209
+ problems.push("claim_scope: a fixed-plan / plan-efficiency event can NEVER be billing-confirmed (contract §4, D5 — hard guardrail, no exceptions)");
210
+ }
211
+ }
212
+ // Defensive label rule: an evidence label that SAYS billing-confirmed on a fixed-plan event is
213
+ // the exact overclaim D5 forbids, whatever the claim_scope field says.
214
+ if (typeof event.evidence_level === "string" &&
215
+ event.evidence_level.toLowerCase().includes("billing-confirmed") &&
216
+ isFixedPlanEvent) {
217
+ problems.push("evidence_level: mentions billing-confirmed on a fixed-plan / plan-efficiency / never-provider-reported event — labels must make this impossible (contract §4, D5)");
218
+ }
219
+ return { problems };
220
+ }
221
+ /* ------------------------------------------------------------------------------------------------
222
+ * Run-record event builders — the writers of the additive `cross_surface_event` field on `run`
223
+ * local run records. Cursor shipped first (D3 alignment, charter 2026-07-03-run-5, #583); the
224
+ * codex + cli writers complete the `run` surfaces (#582 residual #5, same charter). Every writer
225
+ * populates the event from the SAME `RunFlowTokenReport` the record already embeds, so the two can
226
+ * never diverge — and per-axis sources are copied VERBATIM (the builder never launders labels).
227
+ * ---------------------------------------------------------------------------------------------- */
228
+ /** Generic honest fallback when an unavailable axis arrives without its TRUE per-run reason. */
229
+ const AXIS_UNAVAILABLE_FALLBACK = "not safely separable / not reported";
230
+ /** One axis derived from the token report. NEVER relabels: if an upstream source ever wrongly said
231
+ * provider-reported for a never-provider-reported surface (e.g. Cursor), this passes it through
232
+ * and `validateCrossSurfaceEvent` flags it (rule 4) — the builder does not launder labels. */
233
+ function axisFromReport(source, reason) {
234
+ if (source === "unavailable") {
235
+ return { source, unavailable_reason: reason ?? AXIS_UNAVAILABLE_FALLBACK };
236
+ }
237
+ return { source };
238
+ }
239
+ /**
240
+ * Per-surface constraints for the `run` local-run-record writers (contract tier table §3):
241
+ * - `codex` — `run codex`: provider `openai`; token axes are provider-reported where a
242
+ * `turn.completed.usage` block existed (the report says which). Codex reports token usage but
243
+ * never a cost/billing figure, so cost is UNAVAILABLE with that exact reason.
244
+ * - `cursor` — `run cursor`: provider `cursor`; LOCAL-ESTIMATE/unavailable only (D3 ceiling —
245
+ * Cursor emits no usage), and no cost data exists at all.
246
+ * - `cli` — bare `run -- <cmd>`: an arbitrary wrapped command, provider `other` (no provider is
247
+ * involved that Compaction can observe); input is a local chars/4 estimate, output is
248
+ * unavailable with the command-surface reason, and no billing surface exists.
249
+ * - `claude_code` — the `capture claude-code --from-hook` always-on path (charter 2026-07-04-run-8):
250
+ * provider `anthropic`; token axes are PROVIDER-REPORTED where the Claude Code session usage
251
+ * fields carried them (the report says which — a missing axis stays unavailable-with-reason,
252
+ * never a silent zero). Claude Code reports tokens but no cost/billing figure, so cost is
253
+ * UNAVAILABLE with that exact reason. This is NOT invented evidence: the Stop-hook capture reads
254
+ * the session's OWN provider usage metadata — the same provider-reported tokens `capture
255
+ * claude-code` already surfaces — it simply also records a metrics-only activity event so a
256
+ * subsequent session appears in `compaction activity` with no manual import.
257
+ */
258
+ export const RUN_RECORD_EVENT_SURFACES = {
259
+ codex: {
260
+ surface: "codex",
261
+ provider: "openai",
262
+ cost_unavailable_reason: "codex exec --json reports token usage (turn.completed.usage) but no cost or billing figure; no cost data exists on this path"
263
+ },
264
+ claude_code: {
265
+ surface: "claude_code",
266
+ provider: "anthropic",
267
+ cost_unavailable_reason: "Claude Code session usage reports provider tokens (input/output) but no cost or billing figure; no cost data exists on this path"
268
+ },
269
+ cursor: {
270
+ surface: "cursor",
271
+ provider: "cursor",
272
+ cost_unavailable_reason: "Cursor emits no usage or cost data; no cost figure exists for this run"
273
+ },
274
+ cli: {
275
+ surface: "cli",
276
+ provider: "other",
277
+ cost_unavailable_reason: "a bare wrapped command has no provider usage or billing surface; no cost figure exists for this run"
278
+ }
279
+ };
280
+ /**
281
+ * Build the honest cross-surface event for one `run` local run record (any of the three `run`
282
+ * surfaces above):
283
+ * - `surface`/`provider` fixed per surface; `model_label` only as honestly known, else `"unknown"`;
284
+ * - per-axis `token_source` copied VERBATIM from the token report (input and output INDEPENDENT),
285
+ * with the capture's TRUE per-run reason on any unavailable axis (never a bare "unavailable");
286
+ * - counts only on available axes, under the count field that matches the axis tier:
287
+ * `input_before` for the counted/estimated input; a provider-reported output is a COUNT
288
+ * (`output_before`), a local-estimate output is an ESTIMATE and says so (`output_estimate`) —
289
+ * an unavailable axis carries NO count (never a silent zero);
290
+ * - `cost_source: "unavailable"` with the exact per-surface reason (no `run` path has billing data);
291
+ * - `claim_scope: "run-scoped"` (never generalized); caveats = the report's honest notes.
292
+ *
293
+ * The tier ceilings stay intact: this builder never SETS provider-reported (it only copies the
294
+ * report's label), and if a never-provider-reported surface (Cursor) is handed one it passes it
295
+ * through unchanged so the report-only validator rejects it (rule 4) instead of hiding it.
296
+ */
297
+ export function buildRunCrossSurfaceEvent(surfaceKey, params) {
298
+ const spec = RUN_RECORD_EVENT_SURFACES[surfaceKey];
299
+ const report = params.tokenReport;
300
+ const input = axisFromReport(report.input_token_source, params.reasons?.input);
301
+ const output = axisFromReport(report.output_token_source, params.reasons?.output);
302
+ return {
303
+ surface: spec.surface,
304
+ provider: spec.provider,
305
+ model_label: params.modelLabel ?? "unknown",
306
+ run_id: params.runId,
307
+ token_source: { input, output },
308
+ // Counts ride ONLY on an available axis (never a silent zero on an unavailable one).
309
+ ...(input.source !== "unavailable" && report.input_tokens !== undefined ? { input_before: report.input_tokens } : {}),
310
+ ...(output.source === "provider-reported" && report.output_tokens !== undefined
311
+ ? { output_before: report.output_tokens }
312
+ : {}),
313
+ ...(output.source === "local-estimate" && report.output_tokens !== undefined
314
+ ? { output_estimate: report.output_tokens }
315
+ : {}),
316
+ cost_source: "unavailable",
317
+ cost_unavailable_reason: spec.cost_unavailable_reason,
318
+ claim_scope: "run-scoped",
319
+ caveats: [...report.notes]
320
+ };
321
+ }
322
+ /**
323
+ * The Cursor writer (D3 alignment, #583) — kept as the named entry point its call/test sites use;
324
+ * now a thin delegate to the generalized builder with the `cursor` constraints (`surface:
325
+ * "cursor"`, `provider: "cursor"`, `model_label: "unknown"` — never inferred, D3 ceiling intact).
326
+ */
327
+ export function buildCursorRunCrossSurfaceEvent(params) {
328
+ return buildRunCrossSurfaceEvent("cursor", params);
329
+ }
330
+ //# sourceMappingURL=cross-surface-event.js.map
@@ -0,0 +1,54 @@
1
+ import { type UsageMetadata } from "./usage-metadata.js";
2
+ import type { AgentTrace } from "./types.js";
3
+ import { type LocalCommandRun } from "./command-runner.js";
4
+ /**
5
+ * Extract the assistant `result` text + session id from Cursor headless output. Supports the single
6
+ * JSON object (`--output-format json`) and a stream-json stream (scans lines for a `result` object).
7
+ * Returns `outputSeparable: false` when no `result` field can be safely isolated.
8
+ */
9
+ export declare function parseCursorAgentOutput(rawOutput: string): {
10
+ resultText?: string;
11
+ sessionId?: string;
12
+ outputSeparable: boolean;
13
+ };
14
+ /**
15
+ * Best-effort prompt extraction from the wrapped Cursor invocation: the non-flag positional args,
16
+ * excluding the executable + known subcommand keywords. Used ONLY for a local-estimate of input tokens
17
+ * (content-free count of text WE passed). Returns undefined when no prompt can be identified.
18
+ */
19
+ export declare function extractCursorPrompt(commandParts: string[]): string | undefined;
20
+ export interface CursorNormalizationResult {
21
+ trace: AgentTrace;
22
+ usageMetadata: UsageMetadata;
23
+ /** "present" when a prompt was identified (input locally estimated from it), else "unavailable". */
24
+ inputStatus: "present" | "unavailable";
25
+ /**
26
+ * The TRUE reason input is unavailable (set iff `inputStatus === "unavailable"`). Export-only runs
27
+ * (no invocation declared) get a DIFFERENT reason from declared-but-unidentifiable invocations, so the
28
+ * operator is never told "no prompt in the wrapped invocation" when there was no wrapped invocation.
29
+ */
30
+ inputUnavailableReason?: string;
31
+ /** "present" when the result field was separable (output estimated), else "unavailable". */
32
+ outputStatus: "present" | "unavailable";
33
+ /**
34
+ * The TRUE reason output is unavailable (set iff `outputStatus === "unavailable"`), so callers print
35
+ * the honest reason (e.g. "the captured output is empty") — never a hardcoded, possibly-wrong one.
36
+ */
37
+ outputUnavailableReason?: string;
38
+ warnings: string[];
39
+ }
40
+ /** Build an `AgentTrace` + LOCAL-ESTIMATE usage from captured Cursor headless output. */
41
+ export declare function normalizeCursorAgentOutput(params: {
42
+ captureId: string;
43
+ rawOutput: string;
44
+ commandParts?: string[];
45
+ commandRun?: LocalCommandRun;
46
+ generatedAt?: string;
47
+ }): CursorNormalizationResult;
48
+ export interface CursorCaptureResult extends CursorNormalizationResult {
49
+ commandRun?: LocalCommandRun;
50
+ }
51
+ /** Live wrapper: spawn the Cursor headless CLI (no manual export), capture stdout, normalize. */
52
+ export declare function captureCursorCommand(commandParts: string[], generatedAt?: string): Promise<CursorCaptureResult>;
53
+ /** Offline/fallback: normalize a saved Cursor headless output (json / stream-json) string. */
54
+ export declare function captureCursorExport(rawOutput: string, commandParts?: string[], generatedAt?: string): CursorCaptureResult;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Cursor live-wrapper capture (PUBLIC CLI/SDK code — engine-free, ships in the npm package).
3
+ *
4
+ * Wraps a real Cursor **headless CLI agent** run (e.g. `cursor agent -p "<prompt>" --output-format
5
+ * json`) and normalizes its output to an `AgentTrace`. Design: `docs/design/cursor-live-wrapper.md`
6
+ * (accepted 2026-06-28) + the amended honesty matrix in `docs/design/unified-run-flow.md` (founder D2).
7
+ *
8
+ * HONESTY (the defining rails of this module):
9
+ * - **NO provider-reported tokens.** Cursor's CLI emits no usage; tokens here are **LOCAL-ESTIMATE
10
+ * only** (chars/4), never provider-reported, never billing-confirmed.
11
+ * - **Input** is locally estimated from the prompt extracted from the wrapped invocation (where
12
+ * available). **Output** is locally estimated from the headless **`result`** field when the output
13
+ * can be safely separated (`--output-format json`); when it cannot, output is **UNAVAILABLE** (left
14
+ * absent, with a stated reason) — never silently zero, never fabricated.
15
+ * - **NO output-token savings** (that is gated on the output-shaping policy family + eval).
16
+ * - **NO SQLite / private-storage reverse engineering.** Only the wrapped CLI's own stdout is read.
17
+ *
18
+ * Evidence tier: `source: "local_command"` (the same `imported_local` tier as the import path; below
19
+ * `real_captured`). The cross-tool record attributes `tool: "cursor"`, `token_source: local-estimate`.
20
+ */
21
+ import { CURRENT_AGENT_TRACE_ARTIFACT_VERSION } from "./trace-parser.js";
22
+ import { createUsageMetadata } from "./usage-metadata.js";
23
+ import { estimateTextTokens } from "./token-estimator.js";
24
+ import { executeLocalCommand, parseRunCommand } from "./command-runner.js";
25
+ function toRecord(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
27
+ }
28
+ function stringValue(value) {
29
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
30
+ }
31
+ /**
32
+ * Extract the assistant `result` text + session id from Cursor headless output. Supports the single
33
+ * JSON object (`--output-format json`) and a stream-json stream (scans lines for a `result` object).
34
+ * Returns `outputSeparable: false` when no `result` field can be safely isolated.
35
+ */
36
+ export function parseCursorAgentOutput(rawOutput) {
37
+ // A saved export may carry a UTF-8 BOM (e.g. written on Windows). Strip it so a real, parseable
38
+ // `result` object is not silently mislabeled "unavailable" — the label must match the evidence.
39
+ const trimmed = rawOutput.replace(/^\uFEFF/, "").trim();
40
+ // Case 1: a single JSON result object.
41
+ const whole = (() => {
42
+ try {
43
+ return toRecord(JSON.parse(trimmed));
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ })();
49
+ if (whole) {
50
+ const resultText = stringValue(whole.result);
51
+ return { resultText, sessionId: stringValue(whole.session_id), outputSeparable: resultText !== undefined };
52
+ }
53
+ // Case 2: stream-json — scan lines for a `result` object (last one wins) + a session id.
54
+ let resultText;
55
+ let sessionId;
56
+ for (const line of trimmed.split(/\r?\n/)) {
57
+ const t = line.trim();
58
+ if (!t.startsWith("{") || !t.endsWith("}"))
59
+ continue;
60
+ let rec;
61
+ try {
62
+ rec = toRecord(JSON.parse(t));
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ if (!rec)
68
+ continue;
69
+ sessionId = sessionId ?? stringValue(rec.session_id);
70
+ const r = stringValue(rec.result);
71
+ if (r !== undefined)
72
+ resultText = r;
73
+ }
74
+ return { resultText, sessionId, outputSeparable: resultText !== undefined };
75
+ }
76
+ /**
77
+ * Best-effort prompt extraction from the wrapped Cursor invocation: the non-flag positional args,
78
+ * excluding the executable + known subcommand keywords. Used ONLY for a local-estimate of input tokens
79
+ * (content-free count of text WE passed). Returns undefined when no prompt can be identified.
80
+ */
81
+ export function extractCursorPrompt(commandParts) {
82
+ const KEYWORDS = new Set(["cursor", "cursor-agent", "agent", "chat", "exec", "run"]);
83
+ // Boolean flags take no value, so the token after them is still a positional (e.g. the prompt).
84
+ // (Cursor selects machine output via `--output-format json` — a value-taking flag, handled below —
85
+ // so a bare `--json` boolean is intentionally NOT assumed here.)
86
+ const BOOLEAN_FLAGS = new Set(["-p", "--print", "--force"]);
87
+ const positionals = [];
88
+ let skipNext = false;
89
+ for (let i = 1; i < commandParts.length; i++) {
90
+ const part = commandParts[i];
91
+ if (skipNext) {
92
+ skipNext = false; // the value of a preceding value-taking flag (e.g. `--output-format json`)
93
+ continue;
94
+ }
95
+ if (part.startsWith("-")) {
96
+ if (!BOOLEAN_FLAGS.has(part) && !part.includes("="))
97
+ skipNext = true;
98
+ continue;
99
+ }
100
+ if (KEYWORDS.has(part))
101
+ continue;
102
+ positionals.push(part);
103
+ }
104
+ const prompt = positionals.join(" ").trim();
105
+ return prompt === "" ? undefined : prompt;
106
+ }
107
+ function addMessage(messages, role, content, timestamp, metadata) {
108
+ messages.push({ id: `cursor_msg_${messages.length + 1}`, role, content, timestamp, metadata });
109
+ }
110
+ const CURSOR_LOCAL_ESTIMATE_NOTE = "Cursor CLI emits no provider usage; tokens are LOCAL-ESTIMATE only (chars/4) — never provider-reported, never billing-confirmed.";
111
+ /** Build an `AgentTrace` + LOCAL-ESTIMATE usage from captured Cursor headless output. */
112
+ export function normalizeCursorAgentOutput(params) {
113
+ const generatedAt = params.generatedAt ?? new Date().toISOString();
114
+ const startedAt = params.commandRun?.startedAt ?? generatedAt;
115
+ const { resultText, sessionId, outputSeparable } = parseCursorAgentOutput(params.rawOutput);
116
+ const prompt = params.commandParts ? extractCursorPrompt(params.commandParts) : undefined;
117
+ const warnings = [];
118
+ const messages = [];
119
+ addMessage(messages, "system", "Cursor headless CLI capture (command wrapper).", startedAt, {
120
+ integration: "cursor",
121
+ captureMode: "command wrapper"
122
+ });
123
+ if (prompt)
124
+ addMessage(messages, "user", prompt, startedAt, { eventType: "prompt", tokenSource: "local-estimate" });
125
+ if (resultText)
126
+ addMessage(messages, "assistant", resultText, generatedAt, { eventType: "result", tokenSource: "local-estimate" });
127
+ // Input: local-estimate from the prompt where available. Output: local-estimate from the separable
128
+ // result field, else UNAVAILABLE (absent + a stated reason). Never provider-reported, never fabricated.
129
+ const inputTokens = prompt ? estimateTextTokens(prompt) : undefined;
130
+ const outputTokens = resultText ? estimateTextTokens(resultText) : undefined;
131
+ // The TRUE per-run unavailability reason: an EMPTY export/captured output is a different failure from
132
+ // present-but-not-separable output, and the guidance differs (`--output-format json` cannot fix an
133
+ // empty file). Stated exactly so the operator is never sent down the wrong path.
134
+ const outputUnavailableReason = outputSeparable
135
+ ? undefined
136
+ : params.rawOutput.trim() === ""
137
+ ? "the Cursor headless output is empty (no output was captured/saved — nothing to count)"
138
+ : "no separable `result` field in the Cursor headless output (use `--output-format json`)";
139
+ // The TRUE per-run reason input is unavailable. Export-only (no invocation declared at all) is a
140
+ // DIFFERENT situation from a declared invocation in which no prompt could be identified — the reason
141
+ // and the fix differ, so both are stated precisely (never a bare "unknown", never a fabricated count).
142
+ // The documented Cursor headless output (json / stream-json) carries result/session_id/duration —
143
+ // NOT the prompt — so input can never be derived from the export itself without format-guessing.
144
+ const inputUnavailableReason = prompt
145
+ ? undefined
146
+ : params.commandParts === undefined
147
+ ? "a saved Cursor export does not contain the prompt (the documented headless output carries the result, not the user input), and no invocation was declared after --"
148
+ : "no prompt could be identified in the wrapped invocation";
149
+ if (inputUnavailableReason) {
150
+ warnings.push(`Input tokens UNAVAILABLE: ${inputUnavailableReason}.`, params.commandParts === undefined
151
+ ? 'To get a LOCAL-ESTIMATE input count (chars/4 of the prompt YOU declare — never provider-reported), re-declare the original invocation after --: compaction run cursor --out <dir> --export <file> -- cursor agent -p "<your prompt>" --output-format json (same for capture cursor).'
152
+ : 'To get a LOCAL-ESTIMATE input count, pass the prompt in the invocation (e.g. cursor agent -p "<your prompt>" --output-format json).');
153
+ }
154
+ if (outputUnavailableReason) {
155
+ warnings.push(`Output tokens UNAVAILABLE: ${outputUnavailableReason}. ` +
156
+ "Provider-reported output is unavailable (Cursor emits no usage).");
157
+ }
158
+ const limitations = [CURSOR_LOCAL_ESTIMATE_NOTE];
159
+ // Both per-axis unavailability reasons ride in limitations so the SAME reason reaches every surface
160
+ // that renders this capture (CLI stdout notes, the local run record's notes, the summary rollup).
161
+ if (inputUnavailableReason)
162
+ limitations.push(`Input tokens are unavailable for this run (${inputUnavailableReason}).`);
163
+ if (outputUnavailableReason)
164
+ limitations.push(`Output tokens are unavailable for this run (${outputUnavailableReason}).`);
165
+ const usageMetadata = createUsageMetadata({
166
+ ...(inputTokens !== undefined ? { inputTokens } : {}),
167
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
168
+ providerReportedTokens: false,
169
+ estimatedTokens: true,
170
+ provider: "cursor",
171
+ limitations
172
+ });
173
+ const trace = {
174
+ id: sessionId ?? `trace_cursor_${params.captureId}`,
175
+ title: "Cursor headless captured trace",
176
+ artifactVersion: CURRENT_AGENT_TRACE_ARTIFACT_VERSION,
177
+ source: "local_command",
178
+ createdAt: startedAt,
179
+ generatedAt,
180
+ model: "cursor-unknown-model",
181
+ command: params.commandRun
182
+ ? { command: params.commandRun.command.executable, args: params.commandRun.command.args, cwd: process.cwd() }
183
+ : { command: "cursor-export", args: [], cwd: process.cwd() },
184
+ durationMs: params.commandRun?.durationMs ?? 0,
185
+ exitCode: params.commandRun?.exitCode ?? 0,
186
+ messages
187
+ };
188
+ return {
189
+ trace,
190
+ usageMetadata,
191
+ inputStatus: prompt ? "present" : "unavailable",
192
+ ...(inputUnavailableReason !== undefined ? { inputUnavailableReason } : {}),
193
+ outputStatus: outputSeparable ? "present" : "unavailable",
194
+ ...(outputUnavailableReason !== undefined ? { outputUnavailableReason } : {}),
195
+ warnings
196
+ };
197
+ }
198
+ /** Live wrapper: spawn the Cursor headless CLI (no manual export), capture stdout, normalize. */
199
+ export async function captureCursorCommand(commandParts, generatedAt) {
200
+ const parsed = parseRunCommand(commandParts);
201
+ const commandRun = await executeLocalCommand(parsed);
202
+ const result = normalizeCursorAgentOutput({
203
+ captureId: `${parsed.executable}-${commandRun.startedAt}`,
204
+ rawOutput: commandRun.rawOutput,
205
+ commandParts,
206
+ commandRun,
207
+ generatedAt
208
+ });
209
+ return { ...result, commandRun };
210
+ }
211
+ /** Offline/fallback: normalize a saved Cursor headless output (json / stream-json) string. */
212
+ export function captureCursorExport(rawOutput, commandParts, generatedAt) {
213
+ return normalizeCursorAgentOutput({ captureId: "cursor-export", rawOutput, commandParts, generatedAt });
214
+ }
215
+ //# sourceMappingURL=cursor-capture.js.map
@@ -0,0 +1,8 @@
1
+ import type { CursorProbe } from "./cursor-preflight.js";
2
+ /** True when EVERY arg is a recognized safe help/version token (no prompt, no `-p`, no `login`). */
3
+ export declare function isSafeProbeArgs(args: string[]): boolean;
4
+ /**
5
+ * Create the real SAFE probe. The returned probe spawns `<candidate> <args>` ONLY when `args` is a
6
+ * recognized safe help probe; otherwise it refuses (returns `resolved:false` without spawning).
7
+ */
8
+ export declare function createSafeCursorProbe(timeoutMs?: number): CursorProbe;