@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.
- package/README.md +41 -12
- package/dist/cli/commands/activity.d.ts +8 -0
- package/dist/cli/commands/activity.js +35 -0
- package/dist/cli/commands/apply-context.js +68 -0
- package/dist/cli/commands/billing-delta.js +12 -1
- package/dist/cli/commands/capture-claude-code.d.ts +44 -0
- package/dist/cli/commands/capture-claude-code.js +206 -0
- package/dist/cli/commands/capture.js +283 -1
- package/dist/cli/commands/compact.js +9 -5
- package/dist/cli/commands/dev.d.ts +17 -0
- package/dist/cli/commands/dev.js +131 -0
- package/dist/cli/commands/gateway.d.ts +31 -0
- package/dist/cli/commands/gateway.js +312 -0
- package/dist/cli/commands/hooks.d.ts +5 -0
- package/dist/cli/commands/hooks.js +181 -0
- package/dist/cli/commands/init.js +422 -17
- package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
- package/dist/cli/commands/input-compaction-ab.js +125 -0
- package/dist/cli/commands/optimize-hosted.d.ts +54 -0
- package/dist/cli/commands/optimize-hosted.js +123 -0
- package/dist/cli/commands/optimize.js +8 -0
- package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
- package/dist/cli/commands/output-shaping-ab.js +132 -0
- package/dist/cli/commands/output-shaping.d.ts +7 -0
- package/dist/cli/commands/output-shaping.js +43 -0
- package/dist/cli/commands/policies.d.ts +2 -0
- package/dist/cli/commands/policies.js +80 -0
- package/dist/cli/commands/precall.d.ts +19 -0
- package/dist/cli/commands/precall.js +436 -0
- package/dist/cli/commands/recommend.js +1 -1
- package/dist/cli/commands/run.js +297 -12
- package/dist/cli/commands/upgrade-status.d.ts +42 -0
- package/dist/cli/commands/upgrade-status.js +152 -0
- package/dist/cli/cursor-export-read.d.ts +13 -0
- package/dist/cli/cursor-export-read.js +53 -0
- package/dist/cli/cursor-live-preflight.d.ts +19 -0
- package/dist/cli/cursor-live-preflight.js +46 -0
- package/dist/cli/index.js +36 -2
- package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
- package/dist/cli/onboarding/GatewayTui.js +113 -0
- package/dist/cli/onboarding/model.d.ts +32 -1
- package/dist/cli/onboarding/model.js +27 -2
- package/dist/cli/terminal-logo.d.ts +27 -0
- package/dist/cli/terminal-logo.js +49 -0
- package/dist/core/activity-event.d.ts +107 -0
- package/dist/core/activity-event.js +182 -0
- package/dist/core/activity-store.d.ts +63 -0
- package/dist/core/activity-store.js +254 -0
- package/dist/core/activity-view.d.ts +76 -0
- package/dist/core/activity-view.js +120 -0
- package/dist/core/api-client/index.d.ts +3 -1
- package/dist/core/api-client/index.js +4 -0
- package/dist/core/api-client/payload.d.ts +5 -1
- package/dist/core/api-client/payload.js +8 -6
- package/dist/core/api-client/persisted-config.d.ts +57 -0
- package/dist/core/api-client/persisted-config.js +156 -0
- package/dist/core/api-client/tool.d.ts +29 -0
- package/dist/core/api-client/tool.js +50 -0
- package/dist/core/api-client/types.d.ts +15 -4
- package/dist/core/auto-apply-ask.d.ts +58 -0
- package/dist/core/auto-apply-ask.js +105 -0
- package/dist/core/auto-apply-gates.d.ts +76 -0
- package/dist/core/auto-apply-gates.js +113 -0
- package/dist/core/before-call-activity.d.ts +35 -0
- package/dist/core/before-call-activity.js +103 -0
- package/dist/core/before-call-recovery.d.ts +19 -0
- package/dist/core/before-call-recovery.js +46 -0
- package/dist/core/before-call-stdin.d.ts +59 -0
- package/dist/core/before-call-stdin.js +78 -0
- package/dist/core/before-call.d.ts +149 -0
- package/dist/core/before-call.js +358 -0
- package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
- package/dist/core/billing-delta/billing-delta-record.js +26 -0
- package/dist/core/capture-record.d.ts +40 -0
- package/dist/core/capture-record.js +55 -0
- package/dist/core/claude-code-before-call.d.ts +37 -0
- package/dist/core/claude-code-before-call.js +120 -0
- package/dist/core/claude-code-connect.d.ts +31 -0
- package/dist/core/claude-code-connect.js +87 -0
- package/dist/core/claude-code-hook-record.d.ts +71 -0
- package/dist/core/claude-code-hook-record.js +0 -0
- package/dist/core/claude-code-hooks.d.ts +77 -0
- package/dist/core/claude-code-hooks.js +141 -0
- package/dist/core/codex-capture.d.ts +45 -0
- package/dist/core/codex-capture.js +204 -0
- package/dist/core/command-runner.js +5 -1
- package/dist/core/compaction-artifacts.d.ts +51 -0
- package/dist/core/compaction-artifacts.js +242 -0
- package/dist/core/compactor.d.ts +9 -0
- package/dist/core/compactor.js +95 -0
- package/dist/core/cross-surface-event.d.ts +306 -0
- package/dist/core/cross-surface-event.js +330 -0
- package/dist/core/cursor-capture.d.ts +54 -0
- package/dist/core/cursor-capture.js +215 -0
- package/dist/core/cursor-preflight-probe.d.ts +8 -0
- package/dist/core/cursor-preflight-probe.js +88 -0
- package/dist/core/cursor-preflight.d.ts +86 -0
- package/dist/core/cursor-preflight.js +126 -0
- package/dist/core/gateway/apply-activation.d.ts +39 -0
- package/dist/core/gateway/apply-activation.js +84 -0
- package/dist/core/gateway/apply-policy.d.ts +64 -0
- package/dist/core/gateway/apply-policy.js +221 -0
- package/dist/core/gateway/apply-receipt.d.ts +36 -0
- package/dist/core/gateway/apply-receipt.js +75 -0
- package/dist/core/gateway/cache-proof.d.ts +45 -0
- package/dist/core/gateway/cache-proof.js +65 -0
- package/dist/core/gateway/configure.d.ts +50 -0
- package/dist/core/gateway/configure.js +169 -0
- package/dist/core/gateway/openai-usage.d.ts +56 -0
- package/dist/core/gateway/openai-usage.js +128 -0
- package/dist/core/gateway/receipt.d.ts +138 -0
- package/dist/core/gateway/receipt.js +120 -0
- package/dist/core/gateway/recovery.d.ts +23 -0
- package/dist/core/gateway/recovery.js +68 -0
- package/dist/core/gateway/server.d.ts +51 -0
- package/dist/core/gateway/server.js +276 -0
- package/dist/core/gateway/status.d.ts +45 -0
- package/dist/core/gateway/status.js +109 -0
- package/dist/core/hook-usage-aggregate.d.ts +47 -0
- package/dist/core/hook-usage-aggregate.js +161 -0
- package/dist/core/input-compaction-ab.d.ts +111 -0
- package/dist/core/input-compaction-ab.js +158 -0
- package/dist/core/local-run-record.d.ts +109 -0
- package/dist/core/local-run-record.js +223 -0
- package/dist/core/output-shaping-ab.d.ts +140 -0
- package/dist/core/output-shaping-ab.js +146 -0
- package/dist/core/output-shaping-attach.d.ts +31 -0
- package/dist/core/output-shaping-attach.js +57 -0
- package/dist/core/output-shaping.d.ts +56 -0
- package/dist/core/output-shaping.js +89 -0
- package/dist/core/policy-middleware.d.ts +121 -0
- package/dist/core/policy-middleware.js +919 -0
- package/dist/core/policy-preferences.d.ts +99 -0
- package/dist/core/policy-preferences.js +232 -0
- package/dist/core/run-aggregator.d.ts +11 -1
- package/dist/core/run-aggregator.js +29 -2
- package/dist/core/run-flow-report.d.ts +82 -0
- package/dist/core/run-flow-report.js +71 -0
- package/dist/core/safety-report.js +8 -1
- package/dist/core/shim-capture-bridge.d.ts +32 -0
- package/dist/core/shim-capture-bridge.js +88 -0
- package/dist/core/skill-injection-policy.d.ts +72 -0
- package/dist/core/skill-injection-policy.js +183 -0
- package/dist/core/spend-attribution.js +1 -1
- package/dist/core/token-accounting.d.ts +1 -1
- package/dist/core/tool-shim.d.ts +129 -0
- package/dist/core/tool-shim.js +447 -0
- package/dist/core/trace-parser.d.ts +13 -13
- package/dist/core/trace-parser.js +6 -6
- package/dist/core/types.d.ts +12 -2
- package/dist/core/waste-detector.d.ts +20 -0
- package/dist/core/waste-detector.js +160 -6
- 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 {
|
|
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
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const {
|
|
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;
|