@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
@@ -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;
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import chalk from "chalk";
3
3
  import { Option } from "commander";
4
4
  import { writeJsonArtifact, writeTextArtifact } from "../../core/artifact-writer.js";
5
- import { withTraceFingerprint } from "../../core/report-generator.js";
5
+ import { formatCompactionMarkdownReport, withSavingsEvidence, withTraceFingerprint } from "../../core/report-generator.js";
6
6
  import { describeTokenAccounting } from "../../core/token-accounting.js";
7
7
  import { parseTraceFile } from "../../core/trace-parser.js";
8
8
  import { buildRunLabelsFile, hasAnyLabel } from "../../core/run-labels.js";
@@ -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;
@@ -124,11 +125,20 @@ export function registerCompactCommand(program) {
124
125
  // hand-assembled bundle — the eval consumes the identical PolicyMiddlewareResult.
125
126
  const policyResult = applyCompactionPolicy({ trace, compactSkillInjections: approveSkillInjectionPolicy });
126
127
  const artifacts = await writeCompactionArtifacts(trace, options.out, runId, policyResult, undefined, { reviewerType, reviewSummaryPresent, approveSkillInjectionPolicy });
127
- // Attribution/integrity (V0.2 savings-evidence): re-persist report.json with the
128
- // content-addressed trace fingerprint so `aggregate`/`summary` can attribute each saving to a
129
- // verifiable run and de-duplicate the same captured run if it is rolled up twice. Additive,
130
- // backward-compatible; does not change any existing report field/label.
131
- await writeJsonArtifact(options.out, "report.json", withTraceFingerprint(artifacts.report, trace));
128
+ // Attribution/integrity + per-run evidence label (V0.2 savings-evidence): re-persist
129
+ // report.json with (1) the content-addressed trace fingerprint so `aggregate`/`summary` can
130
+ // attribute each saving to a verifiable run and de-duplicate a run rolled up twice, and
131
+ // (2) the composed `savings_evidence` record stating the per-run label explicitly. The
132
+ // compact path's figures are local estimates (chars/4) — `local_estimate` rung 1; never
133
+ // billing-confirmed, never semantic. Additive, backward-compatible; changes no existing field.
134
+ const evidenceReport = withSavingsEvidence(withTraceFingerprint(artifacts.report, trace), {
135
+ costSource: "local_estimate"
136
+ });
137
+ await writeJsonArtifact(options.out, "report.json", evidenceReport);
138
+ // Re-render report.md from the ENRICHED report so the per-run savings_evidence record
139
+ // appears in the human-readable artifact too (the engine rendered report.md from the base
140
+ // report before the CLI added the public-layer fingerprint/evidence enrichment).
141
+ await writeTextArtifact(options.out, "report.md", formatCompactionMarkdownReport(evidenceReport, artifacts.policy));
132
142
  console.log(chalk.cyan("compaction compact"));
133
143
  console.log(artifacts.consoleReport);
134
144
  // Operator-visible only when the approval gesture was given; default output is unchanged.
@@ -180,6 +190,9 @@ export function registerCompactCommand(program) {
180
190
  // (no disk round-trip, no hand-assembled bundle) and print the combined summary. Fail closed:
181
191
  // a failing recoverability verdict exits non-zero, identical to standalone `compaction eval`.
182
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");
183
196
  const evalResult = evaluateCompactionResult(trace, policyResult, runId);
184
197
  printCombinedEvalSummary(evalResult);
185
198
  // Strong eval (Tracks B/C/D): commitment-preservation recoverability + fixture task-check +
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerContextCommand(program: Command): void;
@@ -0,0 +1,130 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import chalk from "chalk";
4
+ import { assembleContext, contextItemsFromTrace, retrieveContextItems } from "../../core/context-store.js";
5
+ import { appendContextItems, loadContextStore } from "../../core/context-store-fs.js";
6
+ /**
7
+ * V0.4 `compaction context` — a LOCAL memory + retrieval surface over the user's own captured
8
+ * traces (design: docs/design/v0.4-context-command.md, accepted 2026-06-24).
9
+ *
10
+ * HARD RAILS (load-bearing): local-only — no network, no model, no embeddings, no engine. It
11
+ * makes NO savings/cost claim (cost lives in `spend`/`summary`) and NO quality/recall/correctness
12
+ * verdict on user data (the four-axis sufficiency harness stays internal — there is no ground
13
+ * truth for an arbitrary user query). `get` prints local retrieval DIAGNOSTICS only.
14
+ */
15
+ const DEFAULT_STORE_DIR = ".compaction/context-store";
16
+ function readTrace(path) {
17
+ if (!existsSync(path)) {
18
+ throw new Error(`File not found: ${path}`);
19
+ }
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(readFileSync(path, "utf8"));
23
+ }
24
+ catch {
25
+ throw new Error(`Not valid JSON: ${path}`);
26
+ }
27
+ const trace = parsed;
28
+ if (!trace || typeof trace.id !== "string" || !Array.isArray(trace.messages)) {
29
+ throw new Error("Not a normalized AgentTrace (expected { id, messages: [...] }). " +
30
+ "Produce one with `compaction capture` or `compaction import`.");
31
+ }
32
+ for (const message of trace.messages) {
33
+ const m = message;
34
+ if (!m || typeof m.id !== "string" || typeof m.content !== "string") {
35
+ throw new Error("Malformed trace: every message needs a string `id` and `content`. " +
36
+ "Produce a trace with `compaction capture` or `compaction import`.");
37
+ }
38
+ }
39
+ return parsed;
40
+ }
41
+ function collect(value, previous) {
42
+ return [...previous, value];
43
+ }
44
+ export function registerContextCommand(program) {
45
+ const context = program
46
+ .command("context")
47
+ .description("Local memory: build a durable context store from your own captured/imported traces and " +
48
+ "retrieve the active context for a next step (local-only; no model, no network, no savings claim).");
49
+ context
50
+ .command("add")
51
+ .argument("<artifact>", "Path to a local normalized AgentTrace JSON (from `compaction capture`/`import`)")
52
+ .option("--store-dir <dir>", "Context store directory (local, gitignored)", DEFAULT_STORE_DIR)
53
+ .description("Append context items derived from a local trace into the local context store (size-cap enforced).")
54
+ .action(async (artifact, options) => {
55
+ const storeDir = resolve(options.storeDir ?? DEFAULT_STORE_DIR);
56
+ const trace = readTrace(resolve(artifact));
57
+ const items = contextItemsFromTrace(trace, { created_at: new Date().toISOString() });
58
+ const result = await appendContextItems(storeDir, items);
59
+ console.log(chalk.cyan("compaction context add"));
60
+ console.log(`Source: ${artifact}`);
61
+ console.log(`Store: ${storeDir}`);
62
+ console.log(`Items added: ${result.appended.length} skipped (duplicate): ${result.skipped.length} ` +
63
+ `evicted (size cap): ${result.evicted.length}`);
64
+ console.log(`Store now holds: ${result.total} items.`);
65
+ console.log("Local-only: nothing was uploaded. This makes no savings claim (see `compaction spend`/`summary` for cost).");
66
+ console.log(chalk.bold('Next: compaction context get "<your query>"'));
67
+ });
68
+ context
69
+ .command("get")
70
+ .argument("<query>", "Free-text description of the current step")
71
+ .option("--store-dir <dir>", "Context store directory (local, gitignored)", DEFAULT_STORE_DIR)
72
+ .option("--budget <tokens>", "Token budget for the assembled context (local estimate)", "4000")
73
+ .option("--source <pointer>", "Required source pointer for coverage diagnostics (repeatable)", collect, [])
74
+ .option("--limit <n>", "Max candidate items to consider after ranking")
75
+ .description("Retrieve + assemble the active context for a query from the local store (deterministic, local). " +
76
+ "Prints the assembled context WITH source pointers + local retrieval diagnostics — not a quality score.")
77
+ .action(async (query, options) => {
78
+ const storeDir = resolve(options.storeDir ?? DEFAULT_STORE_DIR);
79
+ const budget = Number.parseInt(options.budget ?? "4000", 10);
80
+ if (Number.isNaN(budget) || budget < 0) {
81
+ console.error("--budget must be a non-negative integer number of tokens.");
82
+ process.exitCode = 1;
83
+ return;
84
+ }
85
+ let limit;
86
+ if (options.limit !== undefined) {
87
+ limit = Number.parseInt(options.limit, 10);
88
+ if (Number.isNaN(limit) || limit < 0) {
89
+ console.error("--limit must be a non-negative integer.");
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ }
94
+ const items = await loadContextStore(storeDir);
95
+ const retrievalOptions = limit !== undefined ? { limit } : {};
96
+ const start = performance.now();
97
+ const ranked = retrieveContextItems(items, { text: query, source_pointers: options.source && options.source.length > 0 ? options.source : undefined }, retrievalOptions);
98
+ const assembled = assembleContext(ranked, { tokenBudget: budget });
99
+ const latencyMs = performance.now() - start;
100
+ console.log(chalk.cyan("compaction context get"));
101
+ console.log(`Query: ${JSON.stringify(query)}`);
102
+ console.log(`Store: ${storeDir} (${items.length} items)`);
103
+ if (assembled.included.length === 0) {
104
+ console.log("Assembled context: (no matching context in the store for this query)");
105
+ }
106
+ else {
107
+ console.log(`Assembled context (~${assembled.estimated_tokens} tokens, local estimate; budget ${budget}):`);
108
+ for (const item of assembled.included) {
109
+ const pointer = item.source_pointer ?? "(no source pointer)";
110
+ const oneLine = item.content.replace(/\s+/g, " ");
111
+ console.log(` [${pointer}] ${oneLine}`);
112
+ }
113
+ }
114
+ const recoverable = assembled.included.filter((i) => i.recoverability === true && i.source_pointer).length;
115
+ console.log("Diagnostics (local retrieval, not a quality score):");
116
+ console.log(` considered: ${ranked.length} included: ${assembled.included.length} dropped: ${assembled.dropped.length}`);
117
+ console.log(` recoverable (has source pointer): ${recoverable}/${assembled.included.length}`);
118
+ if (options.source && options.source.length > 0) {
119
+ const includedPointers = new Set(assembled.included.map((i) => i.source_pointer).filter((p) => Boolean(p)));
120
+ const covered = options.source.filter((s) => includedPointers.has(s)).length;
121
+ console.log(` source coverage: ${covered}/${options.source.length}`);
122
+ }
123
+ else {
124
+ console.log(" source coverage: n/a (no --source given)");
125
+ }
126
+ console.log(` latency: ${latencyMs.toFixed(1)} ms`);
127
+ console.log("No model, no network. Local memory/retrieval view — it makes no savings claim and no quality/correctness claim.");
128
+ });
129
+ }
130
+ //# sourceMappingURL=context.js.map
@@ -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;