@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.
- package/README.md +142 -64
- 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,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 {
|
|
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;
|