@compaction/cli 0.2.0 → 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 (153) hide show
  1. package/README.md +41 -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 +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
@@ -1,8 +1,110 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import chalk from "chalk";
2
4
  import { captureOpenAIAgentsCommand, captureOpenAIAgentsExport } from "../../core/openai-agents-capture.js";
3
- import { captureClaudeCodeCommand, discoverClaudeCodeCommand } from "./capture-claude-code.js";
5
+ import { captureCodexCommand, captureCodexExport } from "../../core/codex-capture.js";
6
+ import { captureCursorCommand, captureCursorExport } from "../../core/cursor-capture.js";
7
+ import { gateCursorLiveRun } from "../cursor-live-preflight.js";
8
+ import { readCursorExportFile } from "../cursor-export-read.js";
9
+ import { buildRunFlowTokenReport, formatRunFlowTokenReport } from "../../core/run-flow-report.js";
10
+ import { recordCaptureContentFree, captureTokenSource } from "../../core/capture-record.js";
11
+ import { attachOutputShapingToCommand } from "../../core/output-shaping-attach.js";
12
+ import { OUTPUT_SHAPING_HONESTY_NOTE } from "../../core/output-shaping.js";
13
+ import { buildCaptureUsageSidecar } from "../../core/output-shaping-ab.js";
14
+ import { resolveApiConfig } from "../../core/api-client/index.js";
15
+ import { hostedConfigured } from "./optimize-hosted.js";
16
+ import { describeTokenMetadata } from "../../core/usage-metadata.js";
17
+ import { captureClaudeCodeCommand, captureClaudeCodeFromHook, captureClaudeCodeFromPromptHook, discoverClaudeCodeCommand } from "./capture-claude-code.js";
4
18
  import { captureProviderUsageCommand } from "./capture-provider-usage.js";
19
+ import { bridgeCodexShimActivity, bridgeCursorShimActivity } from "../../core/shim-capture-bridge.js";
5
20
  import { DEFAULT_CREDENTIAL_ENV_VAR } from "../../core/provider-usage/provider-usage-adapter.js";
21
+ /**
22
+ * Shared content-free `--from-shim` handler (the PATH-shim always-on bridge — charter 2026-07-04-run-9).
23
+ * Reads the shim's temp copy of the tool's OWN stdout, appends ONE metrics-only activity event, writes
24
+ * NO trace artifact and NO content. Best-effort + fail-open: a bad/missing file is a non-fatal skip so
25
+ * the transparent shim can never break the wrapped command. `commandParts` (Cursor only) carries the
26
+ * ORIGINAL invocation so input can be locally estimated (counted, never stored).
27
+ */
28
+ async function handleCaptureFromShim(tool, fromShimPath, commandParts) {
29
+ let rawOutput;
30
+ try {
31
+ rawOutput = await readFile(fromShimPath, "utf8");
32
+ }
33
+ catch (error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ console.log(`compaction shim: skipped (non-fatal: could not read the captured output — ${message}).`);
36
+ return;
37
+ }
38
+ try {
39
+ if (tool === "codex") {
40
+ const { result, tokenMetadataStatus } = await bridgeCodexShimActivity({ rawOutput, cwd: process.cwd() });
41
+ const label = tokenMetadataStatus === "present" ? "provider-reported" : "usage unavailable (not invented)";
42
+ console.log(result.appended
43
+ ? `compaction shim: recorded metrics-only activity (surface=codex, ${label}, id ${result.activity_event_id.slice(0, 12)}…) — see 'compaction activity'.`
44
+ : `compaction shim: activity not appended (${result.reason}).`);
45
+ }
46
+ else {
47
+ const { result, outputStatus } = await bridgeCursorShimActivity({
48
+ rawOutput,
49
+ commandParts: commandParts.length > 0 ? commandParts : undefined,
50
+ cwd: process.cwd()
51
+ });
52
+ const label = `local-estimate; output ${outputStatus === "present" ? "local-estimate" : "unavailable"}`;
53
+ console.log(result.appended
54
+ ? `compaction shim: recorded metrics-only activity (surface=cursor, ${label}, id ${result.activity_event_id.slice(0, 12)}…) — see 'compaction activity'.`
55
+ : `compaction shim: activity not appended (${result.reason}).`);
56
+ }
57
+ }
58
+ catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ console.log(`compaction shim: skipped (non-fatal: ${message}).`);
61
+ }
62
+ }
63
+ /**
64
+ * Apply the opt-in output-shaping flag to a wrapped command BEFORE generation (increment 3). Returns the
65
+ * (possibly modified) commandParts + console lines. Surfaces NO output-savings number — a savings figure
66
+ * requires the private measured-A/B + short-but-sufficient eval gate. Original command is never mutated.
67
+ */
68
+ function applyOutputShapingFlag(commandParts, options) {
69
+ if (!options.outputShaping)
70
+ return { commandParts, lines: [], policyNames: [] };
71
+ const budget = options.verbosityBudget !== undefined ? Number.parseInt(options.verbosityBudget, 10) : undefined;
72
+ const policies = options.outputShapingPolicies
73
+ ? options.outputShapingPolicies.split(",").map((s) => s.trim()).filter(Boolean)
74
+ : undefined;
75
+ const att = attachOutputShapingToCommand(commandParts, {
76
+ ...(budget !== undefined && Number.isFinite(budget) ? { verbosityBudgetTokens: budget } : {}),
77
+ ...(policies ? { policies } : {})
78
+ });
79
+ const lines = att.attached
80
+ ? [
81
+ chalk.green(` Attached output-shaping policy to the prompt BEFORE generation: ${att.applied.map((a) => a.policy_name).join(", ")}`),
82
+ chalk.gray(` ${OUTPUT_SHAPING_HONESTY_NOTE}`)
83
+ ]
84
+ : [chalk.yellow(` Output-shaping NOT attached: ${att.reason}.`)];
85
+ return { commandParts: att.commandParts, lines, policyNames: att.applied.map((a) => a.policy_name) };
86
+ }
87
+ /**
88
+ * Write a content-free `capture-usage.json` sidecar next to the capture artifact (operator-side evidence).
89
+ * It carries provider-reported token counts + honest source + (treatment) output-shaping policy names so a
90
+ * later `compaction output-shaping-ab add` can link this run into an A/B arm. NO content is written.
91
+ */
92
+ async function writeCaptureUsageSidecar(outDir, tool, usage, policyNames) {
93
+ const sidecar = buildCaptureUsageSidecar({
94
+ tool,
95
+ ...(usage.provider ? { provider: usage.provider } : {}),
96
+ ...(usage.model ? { model: usage.model } : {}),
97
+ ...(typeof usage.input_tokens === "number" ? { inputTokens: usage.input_tokens } : {}),
98
+ ...(typeof usage.output_tokens === "number" ? { outputTokens: usage.output_tokens } : {}),
99
+ providerReported: usage.provider_reported_tokens === true,
100
+ tokenSource: captureTokenSource(usage),
101
+ tokenMetadataStatus: usage.provider_reported_tokens === true ? "present" : "missing",
102
+ ...(policyNames.length > 0 ? { policyNames } : {})
103
+ });
104
+ const sidecarPath = path.join(outDir, "capture-usage.json");
105
+ await writeFile(sidecarPath, JSON.stringify(sidecar, null, 2), "utf8");
106
+ return sidecarPath;
107
+ }
6
108
  export function registerCaptureCommand(program) {
7
109
  const capture = program.command("capture").description("Capture local integration traces into compaction.dev AgentTrace format.");
8
110
  capture
@@ -26,6 +128,175 @@ export function registerCaptureCommand(program) {
26
128
  process.exitCode = 1;
27
129
  }
28
130
  });
131
+ capture
132
+ .command("codex")
133
+ .description("Capture a LIVE `codex exec --json` run (or a saved export) into AgentTrace — no manual import. " +
134
+ "Provider-reported tokens from turn.completed.usage; local only, no upload, no proxying.")
135
+ .option("--out <dir>", "Output directory for capture artifacts")
136
+ .option("--export <file>", "Read a saved `codex exec --json` JSONL export instead of running a command")
137
+ .option("--from-shim <file>", "INTERNAL (used by the connect-once PATH shim): read a captured `codex exec --json` output copy and append ONE metrics-only, CONTENT-FREE activity event (no trace artifact, no content). Fail-open.")
138
+ .option("--output-shaping", "Attach the output-shaping policy to the prompt BEFORE generation (opt-in; no savings claimed)")
139
+ .option("--verbosity-budget <tokens>", "Soft output-token budget for the output-shaping policy")
140
+ .option("--output-shaping-policies <names>", "Comma-separated output-shaping policy names (default: the default-on set)")
141
+ .argument("[commandParts...]", 'Command and args to execute after -- (e.g. -- codex exec --json "do X")')
142
+ .allowUnknownOption(true)
143
+ .action(async (commandPartsRaw, options) => {
144
+ // Always-on shim bridge: content-free, writes only a metrics-only activity event. Handled first,
145
+ // before any artifact-writing path, and before the "provide a command" guard.
146
+ if (options.fromShim) {
147
+ await handleCaptureFromShim("codex", options.fromShim, commandPartsRaw);
148
+ return;
149
+ }
150
+ console.log(chalk.cyan("compaction capture codex"));
151
+ console.log("Capturing a local codex exec --json run. No upload, no proxying. Review the artifact before sharing (it may contain code/output).");
152
+ if (!options.out) {
153
+ console.error("error: --out <dir> is required (except with --from-shim).");
154
+ process.exitCode = 1;
155
+ return;
156
+ }
157
+ if (!options.export && commandPartsRaw.length === 0) {
158
+ console.error('error: provide a command after -- (e.g. -- codex exec --json "do X") or use --export <file>.');
159
+ process.exitCode = 1;
160
+ return;
161
+ }
162
+ const outDir = options.out;
163
+ // Opt-in: attach the output-shaping policy to the prompt BEFORE generation (no savings claimed).
164
+ const shaped = applyOutputShapingFlag(commandPartsRaw, options);
165
+ for (const line of shaped.lines)
166
+ console.log(line);
167
+ const result = options.export
168
+ ? captureCodexExport(await readFile(options.export, "utf8"))
169
+ : await captureCodexCommand(shaped.commandParts);
170
+ await mkdir(outDir, { recursive: true });
171
+ const tracePath = path.join(outDir, "captured-trace.json");
172
+ await writeFile(tracePath, JSON.stringify(result.trace, null, 2), "utf8");
173
+ // Honest token labels: provider-reported (from turn.completed.usage) or honest missing — never invented.
174
+ for (const line of describeTokenMetadata(result.usageMetadata))
175
+ console.log(` ${line}`);
176
+ for (const warning of result.warnings)
177
+ console.log(chalk.yellow(` ! ${warning}`));
178
+ console.log(chalk.green(`Wrote ${tracePath}`));
179
+ // Content-free A/B evidence sidecar (provider-reported counts + policy names) for output-shaping-ab.
180
+ const usagePath = await writeCaptureUsageSidecar(outDir, "codex", result.usageMetadata, shaped.policyNames);
181
+ console.log(chalk.green(`Wrote ${usagePath}`));
182
+ // Unified flow: content-free record (input/output separate + honest source) when hosted-configured.
183
+ // No default network call — only when the user has set COMPACTION_API_URL + COMPACTION_API_KEY.
184
+ if (hostedConfigured()) {
185
+ const rec = await recordCaptureContentFree(resolveApiConfig(), { usage: result.usageMetadata, tool: "codex", reference: tracePath });
186
+ console.log(rec.recorded ? chalk.green(` Recorded content-free usage (tool=codex, source=${rec.source}).`) : chalk.yellow(` Not recorded: ${rec.reason}.`));
187
+ }
188
+ else {
189
+ console.log(" Not recorded (set COMPACTION_API_URL + COMPACTION_API_KEY to record content-free usage).");
190
+ }
191
+ console.log(` Next: compaction compact ${tracePath} --eval --out <dir>`);
192
+ const exitCode = result.commandRun?.exitCode;
193
+ if (typeof exitCode === "number" && exitCode !== 0)
194
+ process.exitCode = exitCode;
195
+ });
196
+ capture
197
+ .command("cursor")
198
+ .description("Capture a LIVE Cursor headless CLI run (or saved output) into AgentTrace — no manual import. " +
199
+ "LOCAL-ESTIMATE tokens only (Cursor emits no usage); output counted from the result field where " +
200
+ "separable (use --output-format json), else marked unavailable. Local only, no upload, no SQLite.")
201
+ .option("--out <dir>", "Output directory for capture artifacts")
202
+ .option("--export <file>", "Read saved Cursor headless output (json / stream-json) instead of running a command")
203
+ .option("--from-shim <file>", "INTERNAL (used by the connect-once PATH shim): read a captured Cursor headless output copy and append ONE metrics-only, CONTENT-FREE activity event (local-estimate only, no trace artifact, no content). Fail-open.")
204
+ .option("--output-shaping", "Attach the output-shaping policy to the prompt BEFORE generation (opt-in; Cursor output is local-estimate, no savings)")
205
+ .option("--verbosity-budget <tokens>", "Soft output-token budget for the output-shaping policy")
206
+ .option("--output-shaping-policies <names>", "Comma-separated output-shaping policy names (default: the default-on set)")
207
+ .argument("[commandParts...]", 'Command and args after -- (e.g. -- cursor agent -p "do X" --output-format json)')
208
+ .allowUnknownOption(true)
209
+ .action(async (commandPartsRaw, options) => {
210
+ // Always-on shim bridge: content-free, writes only a metrics-only activity event (local-estimate).
211
+ // Handled first; the ORIGINAL invocation after -- lets input be locally estimated (counted, not stored).
212
+ if (options.fromShim) {
213
+ await handleCaptureFromShim("cursor", options.fromShim, commandPartsRaw);
214
+ return;
215
+ }
216
+ console.log(chalk.cyan("compaction capture cursor"));
217
+ console.log("Capturing a local Cursor headless run. LOCAL-ESTIMATE tokens only (Cursor emits no provider usage). No upload, no SQLite. Review the artifact before sharing.");
218
+ if (!options.out) {
219
+ console.error("error: --out <dir> is required (except with --from-shim).");
220
+ process.exitCode = 1;
221
+ return;
222
+ }
223
+ if (!options.export && commandPartsRaw.length === 0) {
224
+ console.error('error: provide a command after -- (e.g. -- cursor agent -p "do X" --output-format json) or use --export <file>.');
225
+ process.exitCode = 1;
226
+ return;
227
+ }
228
+ // LIVE path (no --export): run the SAFE preflight FIRST (CLI-presence + capability only; never a
229
+ // real prompt, never `cursor agent login`, never a CURSOR_API_KEY read). If the CLI is missing or
230
+ // not capable, print exact guidance and STOP before spawning — no opaque failure, no crash.
231
+ if (!options.export) {
232
+ const ready = await gateCursorLiveRun();
233
+ if (!ready) {
234
+ process.exitCode = 1;
235
+ return;
236
+ }
237
+ }
238
+ // Opt-in: attach the output-shaping policy to the prompt BEFORE generation. Cursor output stays
239
+ // local-estimate (no provider usage) → shaping never yields a savings number for Cursor.
240
+ const shaped = applyOutputShapingFlag(commandPartsRaw, options);
241
+ for (const line of shaped.lines)
242
+ console.log(line);
243
+ // Export path: read the saved output HONESTLY (a missing/unreadable file is an actionable error,
244
+ // never a raw errno, never a fabricated capture). Pass the RAW declared command (if the operator
245
+ // provided one after --) so the LOCAL-ESTIMATE input count matches `run cursor --export` exactly;
246
+ // shaped parts are NOT used here — nothing was executed, so no shaping was actually attached.
247
+ let result;
248
+ if (options.export) {
249
+ const exportRead = await readCursorExportFile(options.export);
250
+ if (!exportRead.ok) {
251
+ for (const line of exportRead.errorLines)
252
+ console.error(line);
253
+ process.exitCode = 1;
254
+ return;
255
+ }
256
+ result = captureCursorExport(exportRead.rawOutput, commandPartsRaw.length > 0 ? commandPartsRaw : undefined);
257
+ }
258
+ else {
259
+ result = await captureCursorCommand(shaped.commandParts);
260
+ }
261
+ const outDir = options.out;
262
+ await mkdir(outDir, { recursive: true });
263
+ const tracePath = path.join(outDir, "captured-trace.json");
264
+ await writeFile(tracePath, JSON.stringify(result.trace, null, 2), "utf8");
265
+ // Print the SAME honest per-field token block `run cursor` prints (input/output SEPARATE, source
266
+ // explicit, an unavailable axis carries its TRUE per-run reason) — never a bare
267
+ // "input tokens: unknown" with no reason, never a fabricated count, never provider-reported.
268
+ const cursorTokenReport = buildRunFlowTokenReport({
269
+ tool: "cursor",
270
+ usage: result.usageMetadata,
271
+ outputStatus: result.outputStatus
272
+ });
273
+ const cursorReasons = { input: result.inputUnavailableReason, output: result.outputUnavailableReason };
274
+ // `capture` does not compact — no input-reduction figures follow, so that line is omitted.
275
+ const cursorTokenLines = formatRunFlowTokenReport(cursorTokenReport, {
276
+ reasons: cursorReasons,
277
+ inputReductionFollows: false
278
+ });
279
+ for (const line of cursorTokenLines)
280
+ console.log(` ${line}`);
281
+ for (const warning of result.warnings)
282
+ console.log(chalk.yellow(` ! ${warning}`));
283
+ console.log(chalk.green(`Wrote ${tracePath}`));
284
+ // Content-free sidecar. Cursor is local-estimate (providerReported=false) → A/B treats it as
285
+ // unavailable for a provider-reported savings number; it can never produce a confirmed claim.
286
+ const usagePath = await writeCaptureUsageSidecar(outDir, "cursor", result.usageMetadata, shaped.policyNames);
287
+ console.log(chalk.green(`Wrote ${usagePath}`));
288
+ if (hostedConfigured()) {
289
+ const rec = await recordCaptureContentFree(resolveApiConfig(), { usage: result.usageMetadata, tool: "cursor", reference: tracePath });
290
+ console.log(rec.recorded ? chalk.green(` Recorded content-free usage (tool=cursor, source=${rec.source}).`) : chalk.yellow(` Not recorded: ${rec.reason}.`));
291
+ }
292
+ else {
293
+ console.log(" Not recorded (set COMPACTION_API_URL + COMPACTION_API_KEY to record content-free usage).");
294
+ }
295
+ console.log(` Next: compaction compact ${tracePath} --eval --out <dir>`);
296
+ const exitCode = result.commandRun?.exitCode;
297
+ if (typeof exitCode === "number" && exitCode !== 0)
298
+ process.exitCode = exitCode;
299
+ });
29
300
  capture
30
301
  .command("claude-code")
31
302
  .description("Capture a Claude Code session JSONL file into compaction.dev AgentTrace format (source: real_captured). Local only — no network calls, no upload.")
@@ -35,7 +306,18 @@ export function registerCaptureCommand(program) {
35
306
  .option("--out <dir>", "Output directory for captured artifacts (default: .compaction/runs/<session-id-prefix>/)")
36
307
  .option("--max-tool-result-chars <N>", "Maximum characters to extract from each tool result before truncation (default: 32000)")
37
308
  .option("--include-subagents", "Include subagent JSONL files from <session-id>/subagents/ directory, merging their messages and usage into the captured trace")
309
+ .option("--from-hook", "Read a Claude Code Stop payload from stdin and record CONTENT-FREE usage via its transcript_path (used by `compaction hooks install`). Fail-open, idempotent.")
310
+ .option("--from-prompt-hook", "BEFORE-CALL (run-13): read a Claude Code UserPromptSubmit payload from stdin and record a CONTENT-FREE before-call RECOMMENDATION (used by the UserPromptSubmit hook). Recommendation-only (apply is a proven blocker on this surface); the prompt runs unchanged. Fail-open, silent (no stdout).")
311
+ .option("--dry-run", "With --from-hook: print what would be recorded without writing anything.")
38
312
  .action(async (options) => {
313
+ if (options.fromPromptHook) {
314
+ await captureClaudeCodeFromPromptHook();
315
+ return;
316
+ }
317
+ if (options.fromHook) {
318
+ await captureClaudeCodeFromHook({ dryRun: options.dryRun });
319
+ return;
320
+ }
39
321
  if (options.discover) {
40
322
  await discoverClaudeCodeCommand({ projectsDir: options.projectsDir });
41
323
  return;
@@ -109,11 +109,12 @@ export function registerCompactCommand(program) {
109
109
  .description("Compact a trace and write local artifacts.")
110
110
  .action(async (traceFile, options) => {
111
111
  await runEngineCommand(async () => {
112
- // Lazy-load the proprietary engine: present in-repo/API, excluded from the public package.
113
- // When absent, runEngineCommand prints the boundary message and exits cleanly (no stack trace).
114
- const { writeCompactionArtifacts } = await import("../../engine/compaction-artifacts.js");
115
- const { applyCompactionPolicy } = await import("../../engine/policy-middleware.js");
116
- const { evaluateCompactionResult, evaluateStrongCompactionResult, formatStrongEvalMarkdownReport } = await import("../../engine/eval-harness.js");
112
+ // Basic deterministic compaction is PUBLIC (src/core): the transform + policy application +
113
+ // artifact writing ship in the npm package, so `compact` works with no private engine build.
114
+ // Only `--eval` (deterministic recoverability/strong eval) lazy-loads the private engine below;
115
+ // when the engine is absent, runEngineCommand prints the boundary message and exits cleanly.
116
+ const { writeCompactionArtifacts } = await import("../../core/compaction-artifacts.js");
117
+ const { applyCompactionPolicy } = await import("../../core/policy-middleware.js");
117
118
  const trace = await parseTraceFile(traceFile);
118
119
  const reviewerType = options.reviewer;
119
120
  const reviewSummaryPresent = typeof options.reviewSummary === "string" && options.reviewSummary.length > 0;
@@ -189,6 +190,9 @@ export function registerCompactCommand(program) {
189
190
  // (no disk round-trip, no hand-assembled bundle) and print the combined summary. Fail closed:
190
191
  // a failing recoverability verdict exits non-zero, identical to standalone `compaction eval`.
191
192
  if (options.eval === true) {
193
+ // The recoverability/strong eval stays PRIVATE (src/engine): lazy-loaded only when --eval is
194
+ // requested, so basic compaction above never needs the engine. Absent engine ⇒ clean degrade.
195
+ const { evaluateCompactionResult, evaluateStrongCompactionResult, formatStrongEvalMarkdownReport } = await import("../../engine/eval-harness.js");
192
196
  const evalResult = evaluateCompactionResult(trace, policyResult, runId);
193
197
  printCombinedEvalSummary(evalResult);
194
198
  // Strong eval (Tracks B/C/D): commitment-preservation recoverability + fixture task-check +
@@ -0,0 +1,17 @@
1
+ import { Command } from "commander";
2
+ export interface RunThroughGatewayOptions {
3
+ provider?: string;
4
+ upstream?: string;
5
+ listen?: string;
6
+ }
7
+ /**
8
+ * Run `command` through the local Compaction Gateway. Shared by `gateway run` (the public gateway-native
9
+ * form) and the hidden `dev` compatibility alias — `label` only changes the message prefix + usage hint.
10
+ * Calls `process.exit(code)` with the child's exit code (so it is a terminal action).
11
+ */
12
+ export declare function runThroughGateway(command: string[], options: RunThroughGatewayOptions, label?: string): Promise<void>;
13
+ /**
14
+ * Register the hidden `dev` compatibility alias. The PUBLIC gateway-native form is `gateway run`
15
+ * (registered in gateway.ts); `dev` is kept for backward compatibility and hidden from top-level help.
16
+ */
17
+ export declare function registerDevCommand(program: Command): void;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Gateway-native "run a workflow through the gateway" runner (PUBLIC CLI). Charter run-17.1 / run-18A.
3
+ *
4
+ * The public gateway-native entry point is `compaction gateway run -- <command>`. `compaction dev -- …`
5
+ * remains as a hidden compatibility alias (same behavior). Both call `runThroughGateway`.
6
+ *
7
+ * Runs any command with its OpenAI base URL pointed at the local Compaction Gateway, so an
8
+ * OpenAI-compatible app/SDK routes through Compaction with NO manual baseURL copy-paste:
9
+ * compaction gateway run -- npm run dev
10
+ * compaction gateway run -- node app.js
11
+ *
12
+ * Behavior: start (or REUSE) the local gateway → inject the gateway endpoint env into the child (never a
13
+ * key) → run the child with stdio inherited (stdout/stderr/stdin pass through UNCHANGED) → exit with the
14
+ * child's exit code → print a content-free "traffic observed" summary (receipts recorded during this run).
15
+ * It NEVER reads/stores a provider key, NEVER mutates files, and NEVER mutates requests/responses.
16
+ */
17
+ import { spawn } from "node:child_process";
18
+ import { runGatewayStart, parseListen } from "./gateway.js";
19
+ import { getGatewayStatus, readReceipts } from "../../core/gateway/status.js";
20
+ import { summarizeCacheProof } from "../../core/gateway/cache-proof.js";
21
+ import { formatFreshBilledInputReduction } from "../../core/gateway/receipt.js";
22
+ /** The OpenAI-compatible base-url env vars the SDKs read. We set the base only — never a key. */
23
+ function injectionEnv(base) {
24
+ const v1 = `${base}/v1`;
25
+ return { OPENAI_BASE_URL: v1, OPENAI_API_BASE: v1 };
26
+ }
27
+ /** True when SOME provider key is present in the environment (we never read its value). */
28
+ function hasProviderKey(env) {
29
+ return Boolean((env.OPENAI_API_KEY && env.OPENAI_API_KEY.trim()) || (env.OPENAI_KEY && env.OPENAI_KEY.trim()));
30
+ }
31
+ /**
32
+ * Run `command` through the local Compaction Gateway. Shared by `gateway run` (the public gateway-native
33
+ * form) and the hidden `dev` compatibility alias — `label` only changes the message prefix + usage hint.
34
+ * Calls `process.exit(code)` with the child's exit code (so it is a terminal action).
35
+ */
36
+ export async function runThroughGateway(command, options, label = "compaction gateway run") {
37
+ const parts = command ?? [];
38
+ if (parts.length === 0) {
39
+ console.error(`error: no command given. Usage: ${label} -- <command> (e.g. ${label} -- npm run dev)`);
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ const cwd = process.cwd();
44
+ const provider = options.provider ?? "openai";
45
+ const upstream = options.upstream ?? "https://api.openai.com/v1";
46
+ // Reuse a gateway already running in this project, else start our own (ephemeral, quiet).
47
+ const status = await getGatewayStatus(cwd);
48
+ let base;
49
+ let ownGateway = null;
50
+ if (status.running && status.base) {
51
+ base = status.base;
52
+ console.error(`${label}: reusing the running gateway at ${base}`);
53
+ // Honest note: this runner itself always starts in RECORD; if it reuses a gateway the user explicitly
54
+ // started in a mutating mode, say so plainly (it does not silently change that mode).
55
+ if (status.mode && status.mode !== "record") {
56
+ console.error(`${label}: note — the reused gateway is running in '${status.mode}' mode (you started it explicitly); this is not changing that.`);
57
+ }
58
+ }
59
+ else {
60
+ const port = options.listen ? parseListen(options.listen).port : 0; // 0 → ephemeral (no port conflicts)
61
+ try {
62
+ const g = await runGatewayStart({ provider, upstream, mode: "record", host: "127.0.0.1", port, cwd, installSignals: false, log: () => { } });
63
+ base = g.base;
64
+ ownGateway = g;
65
+ console.error(`${label}: started a local gateway at ${base} (record mode; byte-safe; content-free receipts)`);
66
+ }
67
+ catch (err) {
68
+ console.error(`error: could not start a local gateway — ${err.message}`);
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+ }
73
+ const childEnv = { ...process.env, ...injectionEnv(base) };
74
+ console.error(`${label}: routing → OPENAI_BASE_URL=${base}/v1 (your provider key stays on the child; the gateway never stores it)`);
75
+ if (!hasProviderKey(process.env)) {
76
+ console.error(`${label}: Gateway is ready, but the child process still needs its provider API key (e.g. OPENAI_API_KEY). Compaction never injects or stores keys.`);
77
+ }
78
+ // Snapshot receipts so we can report ONLY the traffic observed during this run (content-free).
79
+ const before = new Set(readReceipts(cwd).map((r) => r.receipt_id));
80
+ const child = spawn(parts[0], parts.slice(1), { stdio: "inherit", env: childEnv, cwd });
81
+ const code = await new Promise((resolve) => {
82
+ child.on("exit", (c, signal) => resolve(typeof c === "number" ? c : signal ? 1 : 0));
83
+ child.on("error", (err) => {
84
+ console.error(`${label}: failed to run '${parts[0]}' — ${err.message}`);
85
+ resolve(127);
86
+ });
87
+ });
88
+ if (ownGateway) {
89
+ try {
90
+ await ownGateway.close();
91
+ }
92
+ catch {
93
+ /* best-effort */
94
+ }
95
+ }
96
+ // Content-free traffic summary: only the receipts recorded during this run.
97
+ const observed = readReceipts(cwd).filter((r) => !before.has(r.receipt_id));
98
+ console.error("");
99
+ if (observed.length > 0) {
100
+ const summary = summarizeCacheProof(observed);
101
+ console.error(`${label}: ✓ traffic observed through Compaction — ${observed.length} request(s) this run.`);
102
+ console.error(` best provider-backed fresh/billed input reduction: ${formatFreshBilledInputReduction(summary.bestReduction)}`);
103
+ console.error(" run 'compaction gateway status' for the full content-free rollup.");
104
+ }
105
+ else {
106
+ console.error(`${label}: ○ no requests were observed through Compaction this run.`);
107
+ console.error(" If your app has its own OpenAI base URL configured, it may have bypassed OPENAI_BASE_URL.");
108
+ console.error(" Point your client at the injected base URL, or use 'compaction gateway configure' to set it.");
109
+ }
110
+ process.exit(code);
111
+ }
112
+ /**
113
+ * Register the hidden `dev` compatibility alias. The PUBLIC gateway-native form is `gateway run`
114
+ * (registered in gateway.ts); `dev` is kept for backward compatibility and hidden from top-level help.
115
+ */
116
+ export function registerDevCommand(program) {
117
+ program
118
+ .command("dev")
119
+ .description("Compatibility alias for 'compaction gateway run -- <command>'. Runs a command with its OpenAI base " +
120
+ "URL pointed at the local Compaction Gateway (starts/reuses the gateway, injects the endpoint — never " +
121
+ "a key, streams output unchanged, preserves the exit code, prints a content-free traffic summary).")
122
+ .option("--provider <name>", "Provider (OpenAI-compatible only).", "openai")
123
+ .option("--upstream <url>", "Upstream provider base URL.", "https://api.openai.com/v1")
124
+ .option("--listen <url>", "Local listen address when starting a gateway (default: an ephemeral local port).")
125
+ .argument("[command...]", "The command to run through the gateway (after --), e.g. -- npm run dev")
126
+ .allowUnknownOption(true)
127
+ .action(async (command, options) => {
128
+ await runThroughGateway(command, options, "compaction dev");
129
+ });
130
+ }
131
+ //# sourceMappingURL=dev.js.map
@@ -0,0 +1,31 @@
1
+ import { Command } from "commander";
2
+ import { type GatewayServerMode } from "../../core/gateway/server.js";
3
+ /** Parse `--listen http://127.0.0.1:8787` (or `127.0.0.1:8787`) into host + port. */
4
+ export declare function parseListen(listen: string): {
5
+ host: string;
6
+ port: number;
7
+ };
8
+ export interface GatewayStartConfig {
9
+ provider: string;
10
+ upstream: string;
11
+ mode: GatewayServerMode;
12
+ /** Deterministic apply policy (required for apply/dry-run). Only `deterministic-dedupe` is implemented. */
13
+ policy?: string;
14
+ host: string;
15
+ port: number;
16
+ cwd?: string;
17
+ log?: (line: string) => void;
18
+ /** Install SIGINT/SIGTERM handlers that close the server + remove the pidfile (default true). */
19
+ installSignals?: boolean;
20
+ }
21
+ export interface RunningGateway {
22
+ base: string;
23
+ close: () => Promise<void>;
24
+ }
25
+ /**
26
+ * Start the gateway from a validated config: bind the server, write the content-free pidfile (so
27
+ * `status`/`stop` work), print the honest banner, and install shutdown handlers that remove the pidfile.
28
+ * Reused by both `gateway start` and the guided onboarding. Throws on bind failure (the caller reports it).
29
+ */
30
+ export declare function runGatewayStart(config: GatewayStartConfig): Promise<RunningGateway>;
31
+ export declare function registerGatewayCommand(program: Command): void;