@compaction/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +142 -64
  2. package/dist/cli/commands/activity.d.ts +8 -0
  3. package/dist/cli/commands/activity.js +35 -0
  4. package/dist/cli/commands/apply-context.js +68 -0
  5. package/dist/cli/commands/billing-delta.js +12 -1
  6. package/dist/cli/commands/capture-claude-code.d.ts +44 -0
  7. package/dist/cli/commands/capture-claude-code.js +206 -0
  8. package/dist/cli/commands/capture.js +283 -1
  9. package/dist/cli/commands/compact.js +9 -5
  10. package/dist/cli/commands/dev.d.ts +17 -0
  11. package/dist/cli/commands/dev.js +131 -0
  12. package/dist/cli/commands/gateway.d.ts +31 -0
  13. package/dist/cli/commands/gateway.js +312 -0
  14. package/dist/cli/commands/hooks.d.ts +5 -0
  15. package/dist/cli/commands/hooks.js +181 -0
  16. package/dist/cli/commands/init.js +422 -17
  17. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  18. package/dist/cli/commands/input-compaction-ab.js +125 -0
  19. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  20. package/dist/cli/commands/optimize-hosted.js +123 -0
  21. package/dist/cli/commands/optimize.js +8 -0
  22. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  23. package/dist/cli/commands/output-shaping-ab.js +132 -0
  24. package/dist/cli/commands/output-shaping.d.ts +7 -0
  25. package/dist/cli/commands/output-shaping.js +43 -0
  26. package/dist/cli/commands/policies.d.ts +2 -0
  27. package/dist/cli/commands/policies.js +80 -0
  28. package/dist/cli/commands/precall.d.ts +19 -0
  29. package/dist/cli/commands/precall.js +436 -0
  30. package/dist/cli/commands/recommend.js +1 -1
  31. package/dist/cli/commands/run.js +297 -12
  32. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  33. package/dist/cli/commands/upgrade-status.js +152 -0
  34. package/dist/cli/cursor-export-read.d.ts +13 -0
  35. package/dist/cli/cursor-export-read.js +53 -0
  36. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  37. package/dist/cli/cursor-live-preflight.js +46 -0
  38. package/dist/cli/index.js +36 -2
  39. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  40. package/dist/cli/onboarding/GatewayTui.js +113 -0
  41. package/dist/cli/onboarding/model.d.ts +32 -1
  42. package/dist/cli/onboarding/model.js +27 -2
  43. package/dist/cli/terminal-logo.d.ts +27 -0
  44. package/dist/cli/terminal-logo.js +49 -0
  45. package/dist/core/activity-event.d.ts +107 -0
  46. package/dist/core/activity-event.js +182 -0
  47. package/dist/core/activity-store.d.ts +63 -0
  48. package/dist/core/activity-store.js +254 -0
  49. package/dist/core/activity-view.d.ts +76 -0
  50. package/dist/core/activity-view.js +120 -0
  51. package/dist/core/api-client/index.d.ts +3 -1
  52. package/dist/core/api-client/index.js +4 -0
  53. package/dist/core/api-client/payload.d.ts +5 -1
  54. package/dist/core/api-client/payload.js +8 -6
  55. package/dist/core/api-client/persisted-config.d.ts +57 -0
  56. package/dist/core/api-client/persisted-config.js +156 -0
  57. package/dist/core/api-client/tool.d.ts +29 -0
  58. package/dist/core/api-client/tool.js +50 -0
  59. package/dist/core/api-client/types.d.ts +15 -4
  60. package/dist/core/auto-apply-ask.d.ts +58 -0
  61. package/dist/core/auto-apply-ask.js +105 -0
  62. package/dist/core/auto-apply-gates.d.ts +76 -0
  63. package/dist/core/auto-apply-gates.js +113 -0
  64. package/dist/core/before-call-activity.d.ts +35 -0
  65. package/dist/core/before-call-activity.js +103 -0
  66. package/dist/core/before-call-recovery.d.ts +19 -0
  67. package/dist/core/before-call-recovery.js +46 -0
  68. package/dist/core/before-call-stdin.d.ts +59 -0
  69. package/dist/core/before-call-stdin.js +78 -0
  70. package/dist/core/before-call.d.ts +149 -0
  71. package/dist/core/before-call.js +358 -0
  72. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  73. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  74. package/dist/core/capture-record.d.ts +40 -0
  75. package/dist/core/capture-record.js +55 -0
  76. package/dist/core/claude-code-before-call.d.ts +37 -0
  77. package/dist/core/claude-code-before-call.js +120 -0
  78. package/dist/core/claude-code-connect.d.ts +31 -0
  79. package/dist/core/claude-code-connect.js +87 -0
  80. package/dist/core/claude-code-hook-record.d.ts +71 -0
  81. package/dist/core/claude-code-hook-record.js +0 -0
  82. package/dist/core/claude-code-hooks.d.ts +77 -0
  83. package/dist/core/claude-code-hooks.js +141 -0
  84. package/dist/core/codex-capture.d.ts +45 -0
  85. package/dist/core/codex-capture.js +204 -0
  86. package/dist/core/command-runner.js +5 -1
  87. package/dist/core/compaction-artifacts.d.ts +51 -0
  88. package/dist/core/compaction-artifacts.js +242 -0
  89. package/dist/core/compactor.d.ts +9 -0
  90. package/dist/core/compactor.js +95 -0
  91. package/dist/core/cross-surface-event.d.ts +306 -0
  92. package/dist/core/cross-surface-event.js +330 -0
  93. package/dist/core/cursor-capture.d.ts +54 -0
  94. package/dist/core/cursor-capture.js +215 -0
  95. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  96. package/dist/core/cursor-preflight-probe.js +88 -0
  97. package/dist/core/cursor-preflight.d.ts +86 -0
  98. package/dist/core/cursor-preflight.js +126 -0
  99. package/dist/core/gateway/apply-activation.d.ts +39 -0
  100. package/dist/core/gateway/apply-activation.js +84 -0
  101. package/dist/core/gateway/apply-policy.d.ts +64 -0
  102. package/dist/core/gateway/apply-policy.js +221 -0
  103. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  104. package/dist/core/gateway/apply-receipt.js +75 -0
  105. package/dist/core/gateway/cache-proof.d.ts +45 -0
  106. package/dist/core/gateway/cache-proof.js +65 -0
  107. package/dist/core/gateway/configure.d.ts +50 -0
  108. package/dist/core/gateway/configure.js +169 -0
  109. package/dist/core/gateway/openai-usage.d.ts +56 -0
  110. package/dist/core/gateway/openai-usage.js +128 -0
  111. package/dist/core/gateway/receipt.d.ts +138 -0
  112. package/dist/core/gateway/receipt.js +120 -0
  113. package/dist/core/gateway/recovery.d.ts +23 -0
  114. package/dist/core/gateway/recovery.js +68 -0
  115. package/dist/core/gateway/server.d.ts +51 -0
  116. package/dist/core/gateway/server.js +276 -0
  117. package/dist/core/gateway/status.d.ts +45 -0
  118. package/dist/core/gateway/status.js +109 -0
  119. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  120. package/dist/core/hook-usage-aggregate.js +161 -0
  121. package/dist/core/input-compaction-ab.d.ts +111 -0
  122. package/dist/core/input-compaction-ab.js +158 -0
  123. package/dist/core/local-run-record.d.ts +109 -0
  124. package/dist/core/local-run-record.js +223 -0
  125. package/dist/core/output-shaping-ab.d.ts +140 -0
  126. package/dist/core/output-shaping-ab.js +146 -0
  127. package/dist/core/output-shaping-attach.d.ts +31 -0
  128. package/dist/core/output-shaping-attach.js +57 -0
  129. package/dist/core/output-shaping.d.ts +56 -0
  130. package/dist/core/output-shaping.js +89 -0
  131. package/dist/core/policy-middleware.d.ts +121 -0
  132. package/dist/core/policy-middleware.js +919 -0
  133. package/dist/core/policy-preferences.d.ts +99 -0
  134. package/dist/core/policy-preferences.js +232 -0
  135. package/dist/core/run-aggregator.d.ts +11 -1
  136. package/dist/core/run-aggregator.js +29 -2
  137. package/dist/core/run-flow-report.d.ts +82 -0
  138. package/dist/core/run-flow-report.js +71 -0
  139. package/dist/core/safety-report.js +8 -1
  140. package/dist/core/shim-capture-bridge.d.ts +32 -0
  141. package/dist/core/shim-capture-bridge.js +88 -0
  142. package/dist/core/skill-injection-policy.d.ts +72 -0
  143. package/dist/core/skill-injection-policy.js +183 -0
  144. package/dist/core/spend-attribution.js +1 -1
  145. package/dist/core/token-accounting.d.ts +1 -1
  146. package/dist/core/tool-shim.d.ts +129 -0
  147. package/dist/core/tool-shim.js +447 -0
  148. package/dist/core/trace-parser.d.ts +13 -13
  149. package/dist/core/trace-parser.js +6 -6
  150. package/dist/core/types.d.ts +12 -2
  151. package/dist/core/waste-detector.d.ts +20 -0
  152. package/dist/core/waste-detector.js +160 -6
  153. package/package.json +1 -1
@@ -1,6 +1,18 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import { writeJsonArtifact, writeTextArtifact } from "../../core/artifact-writer.js";
2
4
  import { ClaudeCodeAdapter, PRIVACY_WARNING } from "../../core/adapters/claude-code-adapter.js";
3
5
  import { discoverClaudeCodeSessions, DISCOVERY_PRIVACY_NOTE } from "../../core/adapters/claude-code-discovery.js";
6
+ import { recordCaptureContentFree } from "../../core/capture-record.js";
7
+ import { buildRunFlowTokenReport, formatRunFlowTokenReport } from "../../core/run-flow-report.js";
8
+ import { buildRunCrossSurfaceEvent } from "../../core/cross-surface-event.js";
9
+ import { buildMeasureOnlyActivityEvent } from "../../core/activity-event.js";
10
+ import { appendActivityEvent } from "../../core/activity-store.js";
11
+ import { detectAvoidableContext } from "../../core/before-call.js";
12
+ import { parseUserPromptSubmitPayload, buildClaudeCodeBeforeCallEvent } from "../../core/claude-code-before-call.js";
13
+ import { resolveApiConfig } from "../../core/api-client/index.js";
14
+ import { hostedConfigured } from "./optimize-hosted.js";
15
+ import { buildClaudeCodeHookRecord, computeDedupKey, emptyLedger, isAlreadyRecorded, parseStopPayload, resolveTranscriptPath } from "../../core/claude-code-hook-record.js";
4
16
  function buildOutputDir(sessionPath, out) {
5
17
  if (out)
6
18
  return out;
@@ -120,6 +132,16 @@ export async function captureClaudeCodeCommand(options) {
120
132
  console.log(" figures shown by 'spend' and 'compact' are deterministic LOCAL ESTIMATES (chars/4) — a");
121
133
  console.log(" different measurement, labeled there as local estimates; neither figure is billing-confirmed.");
122
134
  }
135
+ // 6b-iii. Shared UNIFIED-FLOW honest token report (docs/design/unified-run-flow.md, D1/D3): the SAME
136
+ // block that `run codex` / `run cursor` print, so all three tools present ONE honest `token_source`
137
+ // summary. Claude Code usage is PROVIDER-REPORTED and whole (no per-field unavailable split, like
138
+ // Codex), so outputStatus is "present"; when a session lacks provider usage the shared builder honestly
139
+ // downgrades to local-estimate / unavailable — it never labels anything stronger than the evidence. This
140
+ // is a reporting surface only: input/output are shown SEPARATELY with an explicit source, and output is
141
+ // shown as TOKENS ONLY (never a saving). No logic, adapter, or record behavior changes here.
142
+ const runFlowReport = buildRunFlowTokenReport({ tool: "claude-code", usage, outputStatus: "present" });
143
+ for (const line of formatRunFlowTokenReport(runFlowReport))
144
+ console.log(line);
123
145
  // 6c. Per-run distinctness fingerprint (a one-way digest over canonical normalized
124
146
  // content — NOT raw content). Lets a re-capture of the same session be told apart
125
147
  // from a genuinely new one; when unavailable, distinctness is `not_verified`.
@@ -138,6 +160,20 @@ export async function captureClaudeCodeCommand(options) {
138
160
  await writeTextArtifact(outDir, "capture-report.md", markdownReport);
139
161
  // 7. Print artifacts written
140
162
  console.log(`Artifacts written to: ${outDir}/`);
163
+ // Unified flow (docs/design/unified-run-flow.md): content-free record (input/output separate +
164
+ // honest source) when hosted-configured. No default network call — only when COMPACTION_API_URL +
165
+ // COMPACTION_API_KEY are set. Claude Code session usage is provider-reported.
166
+ if (hostedConfigured()) {
167
+ const rec = await recordCaptureContentFree(resolveApiConfig(), {
168
+ usage,
169
+ tool: "claude-code",
170
+ reference: `${outDir}/captured-trace.json`
171
+ });
172
+ console.log(rec.recorded ? `Recorded content-free usage (tool=claude-code, source=${rec.source}).` : `Not recorded: ${rec.reason}.`);
173
+ }
174
+ else {
175
+ console.log("Not recorded (set COMPACTION_API_URL + COMPACTION_API_KEY to record content-free usage).");
176
+ }
141
177
  // 8. Print next step — chain into the verified strong-MVP Claude Code flow:
142
178
  // spend (where the spend goes) → compact --eval (compaction + strong apply-readiness:
143
179
  // deterministic recoverability + commitment-preservation + fixture task-check + token/cost).
@@ -151,6 +187,176 @@ export async function captureClaudeCodeCommand(options) {
151
187
  console.log(" # fixture task-check, and token/cost accounting — nothing is applied automatically.");
152
188
  console.log(` (or: compaction analyze ${outDir}/captured-trace.json for a quick read-only estimate)`);
153
189
  }
190
+ async function readAllStdin() {
191
+ if (process.stdin.isTTY)
192
+ return "";
193
+ const chunks = [];
194
+ for await (const chunk of process.stdin)
195
+ chunks.push(Buffer.from(chunk));
196
+ return Buffer.concat(chunks).toString("utf8");
197
+ }
198
+ async function defaultHookNormalize(transcriptPath) {
199
+ const adapter = new ClaudeCodeAdapter();
200
+ const { trace, usage, provenance } = await adapter.normalize({ sourcePath: transcriptPath });
201
+ return {
202
+ usage,
203
+ messageCount: trace.messages.length,
204
+ ...(provenance.traceFingerprint ? { fingerprint: provenance.traceFingerprint.content_sha256 } : {})
205
+ };
206
+ }
207
+ function hookDir(cwd) {
208
+ return path.join(cwd, ".compaction", "hooks", "claude-code");
209
+ }
210
+ async function loadLedger(cwd) {
211
+ try {
212
+ const raw = await readFile(path.join(hookDir(cwd), "ledger.json"), "utf8");
213
+ const parsed = JSON.parse(raw);
214
+ return Array.isArray(parsed.entries) ? parsed : emptyLedger();
215
+ }
216
+ catch {
217
+ return emptyLedger();
218
+ }
219
+ }
220
+ /**
221
+ * Append ONE metrics-only activity event for a Stop-hook capture (the always-on bridge). Builds the
222
+ * `claude_code` cross-surface event from the SAME honest token report the record uses, then the
223
+ * measure-only activity extension, and appends it to `.compaction/activity/` under the hook's cwd
224
+ * (so tmp-cwd tests and real projects both scope correctly). Content-free and best-effort: any
225
+ * local failure is swallowed here so it can NEVER break the fail-open hook.
226
+ */
227
+ async function appendClaudeCodeHookActivity(input) {
228
+ try {
229
+ // outputStatus "present": buildRunFlowTokenReport honestly downgrades a missing output axis to
230
+ // unavailable — a genuinely-absent field is never labeled provider-reported (no silent zero).
231
+ const tokenReport = buildRunFlowTokenReport({ tool: "claude-code", usage: input.usage, outputStatus: "present" });
232
+ const crossSurfaceEvent = buildRunCrossSurfaceEvent("claude_code", {
233
+ // run_id is the deterministic content-free dedup key → deterministic activity_event_id → the
234
+ // store dedupes a re-captured session (same state = one event).
235
+ runId: `claude-code-${input.dedupKey}`,
236
+ tokenReport,
237
+ reasons: {
238
+ input: "the Claude Code session usage did not report input tokens for this session state",
239
+ output: "the Claude Code session usage did not report output tokens for this session state"
240
+ },
241
+ ...(input.usage.model ? { modelLabel: input.usage.model } : {})
242
+ });
243
+ // session_id rides on the event too (content-free identity), when the Stop payload carried one.
244
+ const eventWithSession = input.sessionId
245
+ ? { ...crossSurfaceEvent, session_id: input.sessionId }
246
+ : crossSurfaceEvent;
247
+ // Measurement only: nothing was applied or retained by Compaction, so original_retained=false.
248
+ const activityEvent = buildMeasureOnlyActivityEvent(eventWithSession, { original_retained: false });
249
+ const result = await appendActivityEvent(activityEvent, path.join(input.cwd, ".compaction", "activity"));
250
+ console.log(result.appended
251
+ ? `compaction hook: recorded metrics-only activity (surface=claude_code, id ${result.activity_event_id.slice(0, 12)}…) — see 'compaction activity'.`
252
+ : `compaction hook: activity not appended (${result.reason}).`);
253
+ }
254
+ catch {
255
+ // Best-effort local append — never fail the fail-open hook on a filesystem/store error.
256
+ }
257
+ }
258
+ /**
259
+ * `compaction capture claude-code --from-hook`. Reads the Stop payload from stdin, resolves the session
260
+ * JSONL via `transcript_path`, and records CONTENT-FREE usage only — never message content, never
261
+ * `last_assistant_message`. Idempotent via a content-free dedup ledger. FAIL-OPEN by contract: any error is
262
+ * swallowed and the process exits 0 so the hook can never break Claude Code.
263
+ */
264
+ export async function captureClaudeCodeFromHook(options = {}, deps = {}) {
265
+ const cwd = deps.cwd ?? process.cwd();
266
+ const now = deps.now ?? (() => new Date().toISOString());
267
+ const readStdin = deps.readStdin ?? readAllStdin;
268
+ const normalize = deps.normalize ?? defaultHookNormalize;
269
+ const hostedIsConfigured = deps.hostedConfigured ?? hostedConfigured;
270
+ try {
271
+ const payload = parseStopPayload(await readStdin());
272
+ const transcriptPath = resolveTranscriptPath(payload);
273
+ if (!transcriptPath) {
274
+ console.log("compaction hook: no transcript_path in the Stop payload — nothing recorded (content-free).");
275
+ return;
276
+ }
277
+ const sessionId = typeof payload?.session_id === "string" ? payload.session_id : undefined;
278
+ const { usage, messageCount, fingerprint } = await normalize(transcriptPath);
279
+ const dedupKey = computeDedupKey({
280
+ sessionId,
281
+ fingerprint,
282
+ messageCount,
283
+ inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : null,
284
+ outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : null
285
+ });
286
+ const ledger = await loadLedger(cwd);
287
+ if (isAlreadyRecorded(ledger, dedupKey)) {
288
+ console.log(`compaction hook: already recorded this session state (dedup ${dedupKey.slice(0, 8)}…) — skipped.`);
289
+ return;
290
+ }
291
+ const record = buildClaudeCodeHookRecord({ usage, ...(sessionId ? { sessionId } : {}), messageCount, dedupKey, recordedAt: now() });
292
+ if (options.dryRun) {
293
+ console.log("compaction hook --dry-run: would record (content-free, nothing written):");
294
+ console.log(JSON.stringify(record, null, 2));
295
+ return;
296
+ }
297
+ const dir = hookDir(cwd);
298
+ await mkdir(path.join(dir, "records"), { recursive: true });
299
+ await writeFile(path.join(dir, "records", `${dedupKey}.json`), `${JSON.stringify(record, null, 2)}\n`, "utf8");
300
+ ledger.entries.push({ dedupKey, ...(sessionId ? { sessionId } : {}), recordedAt: record.recordedAt });
301
+ await writeFile(path.join(dir, "ledger.json"), `${JSON.stringify(ledger, null, 2)}\n`, "utf8");
302
+ console.log(`compaction hook: recorded content-free usage (source=${record.tokenSource}, dedup ${dedupKey.slice(0, 8)}…).`);
303
+ // ALWAYS-ON BRIDGE (charter 2026-07-04-run-8): the Stop hook runs this command at session end,
304
+ // so the SAME content-free usage ALSO appends ONE metrics-only activity event to the local
305
+ // activity store (.compaction/activity/). THIS is what makes a subsequent Claude Code session
306
+ // appear in `compaction activity` with NO manual import. Honesty: surface "claude_code",
307
+ // provider "anthropic", token axes copied VERBATIM from the SAME token report (provider-reported
308
+ // where the session usage carried them, else unavailable-with-reason — never a silent zero);
309
+ // measure-only, so approval "not-required", auto_apply OFF (not eligible, "ask-each-time",
310
+ // applied_automatically false — no auto-apply logic exists), sync "local-only", recovery
311
+ // original_retained=false (measurement only — Compaction modified/retained nothing). The event
312
+ // id is DETERMINISTIC from session state (run_id = the content-free dedup key), so a re-captured
313
+ // session yields the SAME id and the store dedupes it. Best-effort + content-free; a local
314
+ // filesystem failure here never breaks the hook.
315
+ await appendClaudeCodeHookActivity({ cwd, usage, dedupKey, sessionId });
316
+ if (hostedIsConfigured()) {
317
+ const rec = await recordCaptureContentFree(resolveApiConfig(), { usage, tool: "claude-code", reference: `hook:${dedupKey}` });
318
+ console.log(rec.recorded ? `compaction hook: also recorded to hosted (source=${rec.source}).` : `compaction hook: hosted not recorded: ${rec.reason}.`);
319
+ }
320
+ }
321
+ catch (error) {
322
+ // FAIL-OPEN: never propagate — the hook must not break Claude Code.
323
+ const message = error instanceof Error ? error.message : String(error);
324
+ console.log(`compaction hook: skipped (non-fatal: ${message}).`);
325
+ }
326
+ }
327
+ /**
328
+ * `compaction capture claude-code --from-prompt-hook` — the BEFORE-CALL recommendation path (charter
329
+ * 2026-07-04-run-13). The Claude Code **UserPromptSubmit** hook runs this BEFORE the model processes the
330
+ * prompt (a genuine pre-call event — NOT the post-session Stop hook). It reads the UserPromptSubmit
331
+ * payload from stdin, analyzes the pending PROMPT for avoidable duplicated context IN-PROCESS
332
+ * (content-free), and — when present — records ONE metrics-only `claude_code` before-call RECOMMENDATION
333
+ * activity event so it appears in `compaction activity`. Apply is a PROVEN BLOCKER on this surface (no
334
+ * hook can reduce the model's context; hooks are non-interactive) → recommendation-only; the original
335
+ * prompt ALWAYS runs UNCHANGED.
336
+ *
337
+ * CRITICAL: this path writes NOTHING to stdout — on UserPromptSubmit, a hook's stdout is ADDED to the
338
+ * model's context, so injecting anything would be both intrusive and self-defeating for a compaction
339
+ * tool. It is silent and transparent; the recommendation lives in `compaction activity`. FAIL-OPEN +
340
+ * CONTENT-FREE: any error is swallowed and the process exits 0; the prompt text is never persisted.
341
+ */
342
+ export async function captureClaudeCodeFromPromptHook(deps = {}) {
343
+ const readStdin = deps.readStdin ?? readAllStdin;
344
+ try {
345
+ const payload = parseUserPromptSubmitPayload(await readStdin());
346
+ if (!payload)
347
+ return; // malformed/absent payload → nothing recorded (fail-open)
348
+ const result = detectAvoidableContext(payload.prompt);
349
+ if (!result.has_avoidable_context)
350
+ return; // honest no-op → no activity noise, no stdout
351
+ const cwd = payload.cwd ?? deps.cwd ?? process.cwd();
352
+ const event = buildClaudeCodeBeforeCallEvent(result, payload.sessionId ? { sessionId: payload.sessionId } : {});
353
+ await appendActivityEvent(event, path.join(cwd, ".compaction", "activity"));
354
+ // No stdout (would inject into the model context); the recommendation is in `compaction activity`.
355
+ }
356
+ catch {
357
+ // FAIL-OPEN: never propagate — the before-call hook must not break Claude Code.
358
+ }
359
+ }
154
360
  /**
155
361
  * Discover local Claude Code sessions (metadata only) and print, for each, the
156
362
  * ready-to-run `capture claude-code --session <path>` command (guided capture).
@@ -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;