@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
@@ -0,0 +1,54 @@
1
+ import { type UsageMetadata } from "./usage-metadata.js";
2
+ import type { AgentTrace } from "./types.js";
3
+ import { type LocalCommandRun } from "./command-runner.js";
4
+ /**
5
+ * Extract the assistant `result` text + session id from Cursor headless output. Supports the single
6
+ * JSON object (`--output-format json`) and a stream-json stream (scans lines for a `result` object).
7
+ * Returns `outputSeparable: false` when no `result` field can be safely isolated.
8
+ */
9
+ export declare function parseCursorAgentOutput(rawOutput: string): {
10
+ resultText?: string;
11
+ sessionId?: string;
12
+ outputSeparable: boolean;
13
+ };
14
+ /**
15
+ * Best-effort prompt extraction from the wrapped Cursor invocation: the non-flag positional args,
16
+ * excluding the executable + known subcommand keywords. Used ONLY for a local-estimate of input tokens
17
+ * (content-free count of text WE passed). Returns undefined when no prompt can be identified.
18
+ */
19
+ export declare function extractCursorPrompt(commandParts: string[]): string | undefined;
20
+ export interface CursorNormalizationResult {
21
+ trace: AgentTrace;
22
+ usageMetadata: UsageMetadata;
23
+ /** "present" when a prompt was identified (input locally estimated from it), else "unavailable". */
24
+ inputStatus: "present" | "unavailable";
25
+ /**
26
+ * The TRUE reason input is unavailable (set iff `inputStatus === "unavailable"`). Export-only runs
27
+ * (no invocation declared) get a DIFFERENT reason from declared-but-unidentifiable invocations, so the
28
+ * operator is never told "no prompt in the wrapped invocation" when there was no wrapped invocation.
29
+ */
30
+ inputUnavailableReason?: string;
31
+ /** "present" when the result field was separable (output estimated), else "unavailable". */
32
+ outputStatus: "present" | "unavailable";
33
+ /**
34
+ * The TRUE reason output is unavailable (set iff `outputStatus === "unavailable"`), so callers print
35
+ * the honest reason (e.g. "the captured output is empty") — never a hardcoded, possibly-wrong one.
36
+ */
37
+ outputUnavailableReason?: string;
38
+ warnings: string[];
39
+ }
40
+ /** Build an `AgentTrace` + LOCAL-ESTIMATE usage from captured Cursor headless output. */
41
+ export declare function normalizeCursorAgentOutput(params: {
42
+ captureId: string;
43
+ rawOutput: string;
44
+ commandParts?: string[];
45
+ commandRun?: LocalCommandRun;
46
+ generatedAt?: string;
47
+ }): CursorNormalizationResult;
48
+ export interface CursorCaptureResult extends CursorNormalizationResult {
49
+ commandRun?: LocalCommandRun;
50
+ }
51
+ /** Live wrapper: spawn the Cursor headless CLI (no manual export), capture stdout, normalize. */
52
+ export declare function captureCursorCommand(commandParts: string[], generatedAt?: string): Promise<CursorCaptureResult>;
53
+ /** Offline/fallback: normalize a saved Cursor headless output (json / stream-json) string. */
54
+ export declare function captureCursorExport(rawOutput: string, commandParts?: string[], generatedAt?: string): CursorCaptureResult;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Cursor live-wrapper capture (PUBLIC CLI/SDK code — engine-free, ships in the npm package).
3
+ *
4
+ * Wraps a real Cursor **headless CLI agent** run (e.g. `cursor agent -p "<prompt>" --output-format
5
+ * json`) and normalizes its output to an `AgentTrace`. Design: `docs/design/cursor-live-wrapper.md`
6
+ * (accepted 2026-06-28) + the amended honesty matrix in `docs/design/unified-run-flow.md` (founder D2).
7
+ *
8
+ * HONESTY (the defining rails of this module):
9
+ * - **NO provider-reported tokens.** Cursor's CLI emits no usage; tokens here are **LOCAL-ESTIMATE
10
+ * only** (chars/4), never provider-reported, never billing-confirmed.
11
+ * - **Input** is locally estimated from the prompt extracted from the wrapped invocation (where
12
+ * available). **Output** is locally estimated from the headless **`result`** field when the output
13
+ * can be safely separated (`--output-format json`); when it cannot, output is **UNAVAILABLE** (left
14
+ * absent, with a stated reason) — never silently zero, never fabricated.
15
+ * - **NO output-token savings** (that is gated on the output-shaping policy family + eval).
16
+ * - **NO SQLite / private-storage reverse engineering.** Only the wrapped CLI's own stdout is read.
17
+ *
18
+ * Evidence tier: `source: "local_command"` (the same `imported_local` tier as the import path; below
19
+ * `real_captured`). The cross-tool record attributes `tool: "cursor"`, `token_source: local-estimate`.
20
+ */
21
+ import { CURRENT_AGENT_TRACE_ARTIFACT_VERSION } from "./trace-parser.js";
22
+ import { createUsageMetadata } from "./usage-metadata.js";
23
+ import { estimateTextTokens } from "./token-estimator.js";
24
+ import { executeLocalCommand, parseRunCommand } from "./command-runner.js";
25
+ function toRecord(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
27
+ }
28
+ function stringValue(value) {
29
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
30
+ }
31
+ /**
32
+ * Extract the assistant `result` text + session id from Cursor headless output. Supports the single
33
+ * JSON object (`--output-format json`) and a stream-json stream (scans lines for a `result` object).
34
+ * Returns `outputSeparable: false` when no `result` field can be safely isolated.
35
+ */
36
+ export function parseCursorAgentOutput(rawOutput) {
37
+ // A saved export may carry a UTF-8 BOM (e.g. written on Windows). Strip it so a real, parseable
38
+ // `result` object is not silently mislabeled "unavailable" — the label must match the evidence.
39
+ const trimmed = rawOutput.replace(/^\uFEFF/, "").trim();
40
+ // Case 1: a single JSON result object.
41
+ const whole = (() => {
42
+ try {
43
+ return toRecord(JSON.parse(trimmed));
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ })();
49
+ if (whole) {
50
+ const resultText = stringValue(whole.result);
51
+ return { resultText, sessionId: stringValue(whole.session_id), outputSeparable: resultText !== undefined };
52
+ }
53
+ // Case 2: stream-json — scan lines for a `result` object (last one wins) + a session id.
54
+ let resultText;
55
+ let sessionId;
56
+ for (const line of trimmed.split(/\r?\n/)) {
57
+ const t = line.trim();
58
+ if (!t.startsWith("{") || !t.endsWith("}"))
59
+ continue;
60
+ let rec;
61
+ try {
62
+ rec = toRecord(JSON.parse(t));
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ if (!rec)
68
+ continue;
69
+ sessionId = sessionId ?? stringValue(rec.session_id);
70
+ const r = stringValue(rec.result);
71
+ if (r !== undefined)
72
+ resultText = r;
73
+ }
74
+ return { resultText, sessionId, outputSeparable: resultText !== undefined };
75
+ }
76
+ /**
77
+ * Best-effort prompt extraction from the wrapped Cursor invocation: the non-flag positional args,
78
+ * excluding the executable + known subcommand keywords. Used ONLY for a local-estimate of input tokens
79
+ * (content-free count of text WE passed). Returns undefined when no prompt can be identified.
80
+ */
81
+ export function extractCursorPrompt(commandParts) {
82
+ const KEYWORDS = new Set(["cursor", "cursor-agent", "agent", "chat", "exec", "run"]);
83
+ // Boolean flags take no value, so the token after them is still a positional (e.g. the prompt).
84
+ // (Cursor selects machine output via `--output-format json` — a value-taking flag, handled below —
85
+ // so a bare `--json` boolean is intentionally NOT assumed here.)
86
+ const BOOLEAN_FLAGS = new Set(["-p", "--print", "--force"]);
87
+ const positionals = [];
88
+ let skipNext = false;
89
+ for (let i = 1; i < commandParts.length; i++) {
90
+ const part = commandParts[i];
91
+ if (skipNext) {
92
+ skipNext = false; // the value of a preceding value-taking flag (e.g. `--output-format json`)
93
+ continue;
94
+ }
95
+ if (part.startsWith("-")) {
96
+ if (!BOOLEAN_FLAGS.has(part) && !part.includes("="))
97
+ skipNext = true;
98
+ continue;
99
+ }
100
+ if (KEYWORDS.has(part))
101
+ continue;
102
+ positionals.push(part);
103
+ }
104
+ const prompt = positionals.join(" ").trim();
105
+ return prompt === "" ? undefined : prompt;
106
+ }
107
+ function addMessage(messages, role, content, timestamp, metadata) {
108
+ messages.push({ id: `cursor_msg_${messages.length + 1}`, role, content, timestamp, metadata });
109
+ }
110
+ const CURSOR_LOCAL_ESTIMATE_NOTE = "Cursor CLI emits no provider usage; tokens are LOCAL-ESTIMATE only (chars/4) — never provider-reported, never billing-confirmed.";
111
+ /** Build an `AgentTrace` + LOCAL-ESTIMATE usage from captured Cursor headless output. */
112
+ export function normalizeCursorAgentOutput(params) {
113
+ const generatedAt = params.generatedAt ?? new Date().toISOString();
114
+ const startedAt = params.commandRun?.startedAt ?? generatedAt;
115
+ const { resultText, sessionId, outputSeparable } = parseCursorAgentOutput(params.rawOutput);
116
+ const prompt = params.commandParts ? extractCursorPrompt(params.commandParts) : undefined;
117
+ const warnings = [];
118
+ const messages = [];
119
+ addMessage(messages, "system", "Cursor headless CLI capture (command wrapper).", startedAt, {
120
+ integration: "cursor",
121
+ captureMode: "command wrapper"
122
+ });
123
+ if (prompt)
124
+ addMessage(messages, "user", prompt, startedAt, { eventType: "prompt", tokenSource: "local-estimate" });
125
+ if (resultText)
126
+ addMessage(messages, "assistant", resultText, generatedAt, { eventType: "result", tokenSource: "local-estimate" });
127
+ // Input: local-estimate from the prompt where available. Output: local-estimate from the separable
128
+ // result field, else UNAVAILABLE (absent + a stated reason). Never provider-reported, never fabricated.
129
+ const inputTokens = prompt ? estimateTextTokens(prompt) : undefined;
130
+ const outputTokens = resultText ? estimateTextTokens(resultText) : undefined;
131
+ // The TRUE per-run unavailability reason: an EMPTY export/captured output is a different failure from
132
+ // present-but-not-separable output, and the guidance differs (`--output-format json` cannot fix an
133
+ // empty file). Stated exactly so the operator is never sent down the wrong path.
134
+ const outputUnavailableReason = outputSeparable
135
+ ? undefined
136
+ : params.rawOutput.trim() === ""
137
+ ? "the Cursor headless output is empty (no output was captured/saved — nothing to count)"
138
+ : "no separable `result` field in the Cursor headless output (use `--output-format json`)";
139
+ // The TRUE per-run reason input is unavailable. Export-only (no invocation declared at all) is a
140
+ // DIFFERENT situation from a declared invocation in which no prompt could be identified — the reason
141
+ // and the fix differ, so both are stated precisely (never a bare "unknown", never a fabricated count).
142
+ // The documented Cursor headless output (json / stream-json) carries result/session_id/duration —
143
+ // NOT the prompt — so input can never be derived from the export itself without format-guessing.
144
+ const inputUnavailableReason = prompt
145
+ ? undefined
146
+ : params.commandParts === undefined
147
+ ? "a saved Cursor export does not contain the prompt (the documented headless output carries the result, not the user input), and no invocation was declared after --"
148
+ : "no prompt could be identified in the wrapped invocation";
149
+ if (inputUnavailableReason) {
150
+ warnings.push(`Input tokens UNAVAILABLE: ${inputUnavailableReason}.`, params.commandParts === undefined
151
+ ? 'To get a LOCAL-ESTIMATE input count (chars/4 of the prompt YOU declare — never provider-reported), re-declare the original invocation after --: compaction run cursor --out <dir> --export <file> -- cursor agent -p "<your prompt>" --output-format json (same for capture cursor).'
152
+ : 'To get a LOCAL-ESTIMATE input count, pass the prompt in the invocation (e.g. cursor agent -p "<your prompt>" --output-format json).');
153
+ }
154
+ if (outputUnavailableReason) {
155
+ warnings.push(`Output tokens UNAVAILABLE: ${outputUnavailableReason}. ` +
156
+ "Provider-reported output is unavailable (Cursor emits no usage).");
157
+ }
158
+ const limitations = [CURSOR_LOCAL_ESTIMATE_NOTE];
159
+ // Both per-axis unavailability reasons ride in limitations so the SAME reason reaches every surface
160
+ // that renders this capture (CLI stdout notes, the local run record's notes, the summary rollup).
161
+ if (inputUnavailableReason)
162
+ limitations.push(`Input tokens are unavailable for this run (${inputUnavailableReason}).`);
163
+ if (outputUnavailableReason)
164
+ limitations.push(`Output tokens are unavailable for this run (${outputUnavailableReason}).`);
165
+ const usageMetadata = createUsageMetadata({
166
+ ...(inputTokens !== undefined ? { inputTokens } : {}),
167
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
168
+ providerReportedTokens: false,
169
+ estimatedTokens: true,
170
+ provider: "cursor",
171
+ limitations
172
+ });
173
+ const trace = {
174
+ id: sessionId ?? `trace_cursor_${params.captureId}`,
175
+ title: "Cursor headless captured trace",
176
+ artifactVersion: CURRENT_AGENT_TRACE_ARTIFACT_VERSION,
177
+ source: "local_command",
178
+ createdAt: startedAt,
179
+ generatedAt,
180
+ model: "cursor-unknown-model",
181
+ command: params.commandRun
182
+ ? { command: params.commandRun.command.executable, args: params.commandRun.command.args, cwd: process.cwd() }
183
+ : { command: "cursor-export", args: [], cwd: process.cwd() },
184
+ durationMs: params.commandRun?.durationMs ?? 0,
185
+ exitCode: params.commandRun?.exitCode ?? 0,
186
+ messages
187
+ };
188
+ return {
189
+ trace,
190
+ usageMetadata,
191
+ inputStatus: prompt ? "present" : "unavailable",
192
+ ...(inputUnavailableReason !== undefined ? { inputUnavailableReason } : {}),
193
+ outputStatus: outputSeparable ? "present" : "unavailable",
194
+ ...(outputUnavailableReason !== undefined ? { outputUnavailableReason } : {}),
195
+ warnings
196
+ };
197
+ }
198
+ /** Live wrapper: spawn the Cursor headless CLI (no manual export), capture stdout, normalize. */
199
+ export async function captureCursorCommand(commandParts, generatedAt) {
200
+ const parsed = parseRunCommand(commandParts);
201
+ const commandRun = await executeLocalCommand(parsed);
202
+ const result = normalizeCursorAgentOutput({
203
+ captureId: `${parsed.executable}-${commandRun.startedAt}`,
204
+ rawOutput: commandRun.rawOutput,
205
+ commandParts,
206
+ commandRun,
207
+ generatedAt
208
+ });
209
+ return { ...result, commandRun };
210
+ }
211
+ /** Offline/fallback: normalize a saved Cursor headless output (json / stream-json) string. */
212
+ export function captureCursorExport(rawOutput, commandParts, generatedAt) {
213
+ return normalizeCursorAgentOutput({ captureId: "cursor-export", rawOutput, commandParts, generatedAt });
214
+ }
215
+ //# sourceMappingURL=cursor-capture.js.map
@@ -0,0 +1,8 @@
1
+ import type { CursorProbe } from "./cursor-preflight.js";
2
+ /** True when EVERY arg is a recognized safe help/version token (no prompt, no `-p`, no `login`). */
3
+ export declare function isSafeProbeArgs(args: string[]): boolean;
4
+ /**
5
+ * Create the real SAFE probe. The returned probe spawns `<candidate> <args>` ONLY when `args` is a
6
+ * recognized safe help probe; otherwise it refuses (returns `resolved:false` without spawning).
7
+ */
8
+ export declare function createSafeCursorProbe(timeoutMs?: number): CursorProbe;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * SAFE real probe adapter for the Cursor live-prep preflight (`cursor-preflight.ts`).
3
+ *
4
+ * This is the ONLY place the preflight spawns a child process, and it spawns ONLY a `--help`/`-v`-style
5
+ * capability probe. Defense-in-depth: it hard-rejects any arg set that is not a recognized safe help
6
+ * probe, so it can NEVER be coaxed into a real `-p` prompt, `cursor agent login`, or a credential read.
7
+ *
8
+ * - No stdin (closed): a probe never waits for input.
9
+ * - stdout/stderr captured only to inspect for the headless flags in --help text (content-free).
10
+ * - CURSOR_API_KEY is never read, set, or forwarded specially — the probe inherits the caller's env
11
+ * unchanged (we never touch the key), and never prints env values.
12
+ * - Fail-friendly: an ENOENT / spawn error resolves to `resolved:false`, never a throw.
13
+ */
14
+ import { spawn } from "node:child_process";
15
+ /** Only these tokens may appear in a probe arg list. Anything else is refused (never spawned). */
16
+ const SAFE_PROBE_TOKENS = new Set(["agent", "--help", "-h", "--version", "-v"]);
17
+ /** True when EVERY arg is a recognized safe help/version token (no prompt, no `-p`, no `login`). */
18
+ export function isSafeProbeArgs(args) {
19
+ if (args.length === 0)
20
+ return false;
21
+ return args.every((a) => SAFE_PROBE_TOKENS.has(a));
22
+ }
23
+ /**
24
+ * Create the real SAFE probe. The returned probe spawns `<candidate> <args>` ONLY when `args` is a
25
+ * recognized safe help probe; otherwise it refuses (returns `resolved:false` without spawning).
26
+ */
27
+ export function createSafeCursorProbe(timeoutMs = 5000) {
28
+ return (candidate, args) => {
29
+ return new Promise((resolve) => {
30
+ const notRun = (helpText) => ({ candidate, resolved: false, helpText, exitCode: null });
31
+ // Defense-in-depth: refuse anything that is not a pure help/version probe.
32
+ if (!isSafeProbeArgs(args)) {
33
+ resolve(notRun("refused: non-help probe args (safety guard)"));
34
+ return;
35
+ }
36
+ let helpText = "";
37
+ let settled = false;
38
+ let child;
39
+ try {
40
+ child = spawn(candidate, args, {
41
+ shell: false,
42
+ windowsHide: true,
43
+ // Closed stdin so a probe never blocks waiting for input; capture stdout/stderr to read --help.
44
+ stdio: ["ignore", "pipe", "pipe"]
45
+ });
46
+ }
47
+ catch {
48
+ resolve(notRun(""));
49
+ return;
50
+ }
51
+ const finish = (result) => {
52
+ if (settled)
53
+ return;
54
+ settled = true;
55
+ resolve(result);
56
+ };
57
+ const timer = setTimeout(() => {
58
+ try {
59
+ child.kill("SIGKILL");
60
+ }
61
+ catch {
62
+ /* ignore */
63
+ }
64
+ // A hung help probe: treat as resolved (it spawned) but with whatever text we saw.
65
+ finish({ candidate, resolved: true, helpText, exitCode: null });
66
+ }, timeoutMs);
67
+ timer.unref?.();
68
+ child.stdout?.setEncoding("utf8");
69
+ child.stderr?.setEncoding("utf8");
70
+ child.stdout?.on("data", (c) => {
71
+ helpText += c;
72
+ });
73
+ child.stderr?.on("data", (c) => {
74
+ helpText += c;
75
+ });
76
+ child.on("error", () => {
77
+ // ENOENT / not executable → the binary is not resolvable. Fail-friendly, never throw.
78
+ clearTimeout(timer);
79
+ finish(notRun(""));
80
+ });
81
+ child.on("close", (exitCode) => {
82
+ clearTimeout(timer);
83
+ finish({ candidate, resolved: true, helpText, exitCode: exitCode ?? null });
84
+ });
85
+ });
86
+ };
87
+ }
88
+ //# sourceMappingURL=cursor-preflight-probe.js.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Cursor LIVE-PREP preflight/diagnostics (PUBLIC CLI code — engine-free, ships in the npm package).
3
+ *
4
+ * A content-free readiness check surfaced when a LIVE `run cursor` / `capture cursor` is requested
5
+ * (no `--export`). It answers two questions and prints exact actionable guidance so a live Cursor run
6
+ * validates cleanly once the operator authenticates — WITHOUT this tool ever authenticating or spending:
7
+ *
8
+ * 1. **CLI resolvable?** Is a Cursor headless CLI binary on PATH (`cursor` / `cursor-agent`) or at a
9
+ * known macOS app path (`/Applications/Cursor.app/Contents/Resources/app/bin/cursor`)?
10
+ * 2. **Capability present?** Does `<cli> agent --help` (or `--help`) advertise the headless flags we
11
+ * need (`-p`/`--print` + `--output-format`)?
12
+ *
13
+ * HARD SAFETY RAILS (the defining constraints of this module):
14
+ * - **SAFE probes only.** Probing is limited to `--help` / `-v` / `--version` style invocations. This
15
+ * module NEVER runs a real `-p` prompt, NEVER runs `cursor agent login`, and NEVER reads, sets, or
16
+ * prints `CURSOR_API_KEY`.
17
+ * - **Auth-state is NOT detected.** Cursor's auth state cannot be observed without a real (spending)
18
+ * call, so this module detects **CLI-presence + capability only** and tells the operator to run
19
+ * `cursor agent login` (or set `CURSOR_API_KEY`) and retry. It never guesses "authenticated".
20
+ * - **Fail friendly.** A probe that errors/crashes is treated as "not detectable", never a thrown
21
+ * exception — the pure logic here is total over its synthetic inputs.
22
+ * - **Content-free.** No prompt, no output, no credential value is ever read or emitted.
23
+ *
24
+ * Honesty binding (unchanged): Cursor = LOCAL-ESTIMATE only; provider-reported UNAVAILABLE; never a
25
+ * savings claim. This module surfaces none of those figures — it is a readiness check + guidance only.
26
+ *
27
+ * Design: `docs/design/cursor-live-wrapper.md` (Verify-first precondition V) + the honesty rails in
28
+ * `docs/ops/cursor-support-matrix.md`.
29
+ */
30
+ /** Candidate CLI locations, in probe order. Kept as data so the pure logic is fully testable. */
31
+ export declare const CURSOR_CLI_PATH_CANDIDATES: readonly ["cursor-agent", "cursor"];
32
+ export declare const CURSOR_CLI_KNOWN_MACOS_PATH = "/Applications/Cursor.app/Contents/Resources/app/bin/cursor";
33
+ /** The exact command an operator runs once authed (surfaced verbatim in the guidance). */
34
+ export declare const CURSOR_EXAMPLE_RUN_COMMAND = "compaction run cursor --out <dir> -- cursor agent -p \"<your prompt>\" --output-format json";
35
+ /**
36
+ * Result of a single SAFE probe of one candidate binary. `helpText` comes from a `--help`/`-v`-style
37
+ * invocation only. `resolved` is false when the binary could not be spawned at all (ENOENT / not
38
+ * executable). This is the ONLY input the pure classifier consumes — so tests can feed synthetic probe
39
+ * outputs with no live `cursor agent` call.
40
+ */
41
+ export interface CursorProbeResult {
42
+ /** The candidate that was probed (a PATH name or an absolute path). */
43
+ candidate: string;
44
+ /** True when the binary spawned (even if it exited non-zero); false on ENOENT / spawn failure. */
45
+ resolved: boolean;
46
+ /** Combined help/version text observed from the SAFE probe (stdout + stderr). Content-free (help text). */
47
+ helpText: string;
48
+ /** Exit code of the SAFE probe, or null if it never ran. */
49
+ exitCode: number | null;
50
+ }
51
+ /**
52
+ * A probe function: given a candidate + safe args, returns what the SAFE probe observed. Injected so
53
+ * the classifier is pure/testable; the real adapter (`createSafeCursorProbe`) spawns `--help`/`-v` only.
54
+ */
55
+ export type CursorProbe = (candidate: string, args: string[]) => Promise<CursorProbeResult>;
56
+ export interface CursorPreflightReport {
57
+ /** Whether a Cursor CLI binary could be resolved (PATH or known macOS app path). */
58
+ cliResolvable: boolean;
59
+ /** The resolved binary path/name, if any. */
60
+ resolvedCli?: string;
61
+ /** Whether the resolved CLI advertises the headless capability (`-p`/`--print` + `--output-format`). */
62
+ capabilityPresent: boolean;
63
+ /**
64
+ * Auth is intentionally NEVER probed (would require a real, spending call). Always "not-checked" so
65
+ * no caller can mistake this for an "authenticated" signal.
66
+ */
67
+ authState: "not-checked";
68
+ /** Overall readiness of the LOCAL PREP surface: ready = CLI resolvable AND capability present. */
69
+ ready: boolean;
70
+ /** Human-readable, content-free diagnostic + guidance lines (safe to print to stdout). */
71
+ guidance: string[];
72
+ }
73
+ /**
74
+ * PURE classifier: given the SAFE probe results for the CLI candidates, decide resolvable/capable and
75
+ * build the exact guidance. No I/O, no spawning, no env reads — this is what the tests target directly.
76
+ *
77
+ * A candidate is "capable" when its help text advertises BOTH `-p`/`--print` AND `--output-format`.
78
+ */
79
+ export declare function classifyCursorPreflight(probes: CursorProbeResult[]): CursorPreflightReport;
80
+ /**
81
+ * Run the preflight using an injectable probe. Tries each candidate with a SAFE capability probe
82
+ * (`agent --help`), then classifies. Pure over `probe` — the default real adapter
83
+ * (`createSafeCursorProbe`) is the only place a child process is spawned, and only with `--help`-style
84
+ * args. The candidate order puts the known macOS app path LAST as a fallback when PATH names miss.
85
+ */
86
+ export declare function runCursorPreflight(probe: CursorProbe, candidates?: readonly string[]): Promise<CursorPreflightReport>;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Cursor LIVE-PREP preflight/diagnostics (PUBLIC CLI code — engine-free, ships in the npm package).
3
+ *
4
+ * A content-free readiness check surfaced when a LIVE `run cursor` / `capture cursor` is requested
5
+ * (no `--export`). It answers two questions and prints exact actionable guidance so a live Cursor run
6
+ * validates cleanly once the operator authenticates — WITHOUT this tool ever authenticating or spending:
7
+ *
8
+ * 1. **CLI resolvable?** Is a Cursor headless CLI binary on PATH (`cursor` / `cursor-agent`) or at a
9
+ * known macOS app path (`/Applications/Cursor.app/Contents/Resources/app/bin/cursor`)?
10
+ * 2. **Capability present?** Does `<cli> agent --help` (or `--help`) advertise the headless flags we
11
+ * need (`-p`/`--print` + `--output-format`)?
12
+ *
13
+ * HARD SAFETY RAILS (the defining constraints of this module):
14
+ * - **SAFE probes only.** Probing is limited to `--help` / `-v` / `--version` style invocations. This
15
+ * module NEVER runs a real `-p` prompt, NEVER runs `cursor agent login`, and NEVER reads, sets, or
16
+ * prints `CURSOR_API_KEY`.
17
+ * - **Auth-state is NOT detected.** Cursor's auth state cannot be observed without a real (spending)
18
+ * call, so this module detects **CLI-presence + capability only** and tells the operator to run
19
+ * `cursor agent login` (or set `CURSOR_API_KEY`) and retry. It never guesses "authenticated".
20
+ * - **Fail friendly.** A probe that errors/crashes is treated as "not detectable", never a thrown
21
+ * exception — the pure logic here is total over its synthetic inputs.
22
+ * - **Content-free.** No prompt, no output, no credential value is ever read or emitted.
23
+ *
24
+ * Honesty binding (unchanged): Cursor = LOCAL-ESTIMATE only; provider-reported UNAVAILABLE; never a
25
+ * savings claim. This module surfaces none of those figures — it is a readiness check + guidance only.
26
+ *
27
+ * Design: `docs/design/cursor-live-wrapper.md` (Verify-first precondition V) + the honesty rails in
28
+ * `docs/ops/cursor-support-matrix.md`.
29
+ */
30
+ /** Candidate CLI locations, in probe order. Kept as data so the pure logic is fully testable. */
31
+ export const CURSOR_CLI_PATH_CANDIDATES = ["cursor-agent", "cursor"];
32
+ export const CURSOR_CLI_KNOWN_MACOS_PATH = "/Applications/Cursor.app/Contents/Resources/app/bin/cursor";
33
+ /** The exact command an operator runs once authed (surfaced verbatim in the guidance). */
34
+ export const CURSOR_EXAMPLE_RUN_COMMAND = 'compaction run cursor --out <dir> -- cursor agent -p "<your prompt>" --output-format json';
35
+ /** True when help text advertises the headless print flag (`-p` or `--print`). */
36
+ function hasPrintFlag(helpText) {
37
+ return /(^|\s)-p(\s|,|$)/.test(helpText) || /--print\b/.test(helpText);
38
+ }
39
+ /** True when help text advertises `--output-format` (the flag that makes output separable). */
40
+ function hasOutputFormatFlag(helpText) {
41
+ return /--output-format\b/.test(helpText);
42
+ }
43
+ /**
44
+ * PURE classifier: given the SAFE probe results for the CLI candidates, decide resolvable/capable and
45
+ * build the exact guidance. No I/O, no spawning, no env reads — this is what the tests target directly.
46
+ *
47
+ * A candidate is "capable" when its help text advertises BOTH `-p`/`--print` AND `--output-format`.
48
+ */
49
+ export function classifyCursorPreflight(probes) {
50
+ const resolvedProbes = probes.filter((p) => p.resolved);
51
+ const cliResolvable = resolvedProbes.length > 0;
52
+ // Prefer a resolved candidate that ALSO shows the capability; else the first resolved one.
53
+ const capableProbe = resolvedProbes.find((p) => hasPrintFlag(p.helpText) && hasOutputFormatFlag(p.helpText));
54
+ const resolvedCli = capableProbe?.candidate ?? resolvedProbes[0]?.candidate;
55
+ const capabilityPresent = capableProbe !== undefined;
56
+ const ready = cliResolvable && capabilityPresent;
57
+ const guidance = [];
58
+ guidance.push("Cursor live-run preflight (content-free; no prompt run, no login, no API key read):");
59
+ guidance.push(` - CLI resolvable: ${cliResolvable ? `yes (${resolvedCli})` : "no"}`);
60
+ guidance.push(` - Headless capability (-p/--print + --output-format): ${capabilityPresent ? "yes" : cliResolvable ? "not detected in --help" : "n/a (CLI not resolved)"}`);
61
+ // Auth is deliberately NOT probed — say so explicitly so nobody reads readiness as "authenticated".
62
+ guidance.push(" - Auth: NOT checked here (detecting it needs a real, spending call). After the steps below, run `cursor agent login` (or set CURSOR_API_KEY) and retry.");
63
+ if (!cliResolvable) {
64
+ guidance.push("");
65
+ guidance.push("Next steps — the Cursor headless CLI was not found:");
66
+ guidance.push(" 1. Install / locate it: install the Cursor CLI (see cursor.com docs), or if the Cursor app is installed on macOS it ships at:");
67
+ guidance.push(` ${CURSOR_CLI_KNOWN_MACOS_PATH}`);
68
+ guidance.push(" Ensure `cursor` or `cursor-agent` is on your PATH (the `agent` subcommand provides `-p`/`--output-format`).");
69
+ guidance.push(" 2. Authenticate (does NOT happen automatically here): run `cursor agent login`, OR export CURSOR_API_KEY in your shell.");
70
+ guidance.push(" 3. Then run the live flow:");
71
+ guidance.push(` ${CURSOR_EXAMPLE_RUN_COMMAND}`);
72
+ }
73
+ else if (!capabilityPresent) {
74
+ guidance.push("");
75
+ guidance.push(`Next steps — a Cursor CLI was found (${resolvedCli}) but its --help did not advertise the headless flags (-p/--print + --output-format):`);
76
+ guidance.push(" - This may be the Cursor editor launcher rather than the headless agent CLI. Install/verify the headless `cursor agent` CLI, then retry.");
77
+ guidance.push(" - Authenticate first (not done here): `cursor agent login`, OR set CURSOR_API_KEY.");
78
+ guidance.push(" - Then run the live flow:");
79
+ guidance.push(` ${CURSOR_EXAMPLE_RUN_COMMAND}`);
80
+ }
81
+ else {
82
+ guidance.push("");
83
+ guidance.push("The Cursor headless CLI is present and capable. Auth is the only remaining prerequisite (not checked here):");
84
+ guidance.push(" 1. Authenticate: run `cursor agent login`, OR export CURSOR_API_KEY.");
85
+ guidance.push(" 2. Then run the live flow:");
86
+ guidance.push(` ${CURSOR_EXAMPLE_RUN_COMMAND}`);
87
+ guidance.push(" Note: Cursor tokens are LOCAL-ESTIMATE only (Cursor emits no provider usage) — provider-reported tokens are unavailable and no savings is claimed.");
88
+ }
89
+ return {
90
+ cliResolvable,
91
+ ...(resolvedCli !== undefined ? { resolvedCli } : {}),
92
+ capabilityPresent,
93
+ authState: "not-checked",
94
+ ready,
95
+ guidance
96
+ };
97
+ }
98
+ /**
99
+ * Run the preflight using an injectable probe. Tries each candidate with a SAFE capability probe
100
+ * (`agent --help`), then classifies. Pure over `probe` — the default real adapter
101
+ * (`createSafeCursorProbe`) is the only place a child process is spawned, and only with `--help`-style
102
+ * args. The candidate order puts the known macOS app path LAST as a fallback when PATH names miss.
103
+ */
104
+ export async function runCursorPreflight(probe, candidates = [...CURSOR_CLI_PATH_CANDIDATES, CURSOR_CLI_KNOWN_MACOS_PATH]) {
105
+ const probes = [];
106
+ for (const candidate of candidates) {
107
+ // SAFE capability probes ONLY. Never a real prompt, never `login`. We try BOTH help forms because
108
+ // the flags live under different help surfaces across shapes: the standalone `cursor-agent` binary
109
+ // IS the agent (headless flags on its top-level `--help`), while the editor `cursor` launcher
110
+ // exposes them under the `agent` subcommand (`cursor agent --help`). A candidate that resolves in
111
+ // EITHER probe counts as resolved; its help text is the one that best advertises the capability.
112
+ const subcommandHelp = await probe(candidate, ["agent", "--help"]);
113
+ const topLevelHelp = await probe(candidate, ["--help"]);
114
+ probes.push(mergeCandidateProbes(candidate, [subcommandHelp, topLevelHelp]));
115
+ }
116
+ return classifyCursorPreflight(probes);
117
+ }
118
+ /** Merge the help forms tried for one candidate: resolved if EITHER spawned; keep the help text that
119
+ * advertises the capability (so a candidate is judged capable if ANY safe help form shows the flags). */
120
+ function mergeCandidateProbes(candidate, forms) {
121
+ const resolved = forms.some((f) => f.resolved);
122
+ const capableForm = forms.find((f) => f.resolved && hasPrintFlag(f.helpText) && hasOutputFormatFlag(f.helpText));
123
+ const chosen = capableForm ?? forms.find((f) => f.resolved) ?? forms[0];
124
+ return { candidate, resolved, helpText: chosen?.helpText ?? "", exitCode: chosen?.exitCode ?? null };
125
+ }
126
+ //# sourceMappingURL=cursor-preflight.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Compaction Gateway APPLY activation resolver (PUBLIC CLI/SDK core — engine-free). Charter run-17.2.
3
+ *
4
+ * Apply mode is OPT-IN and EXPLICIT. It activates only through explicit user intent — the gateway's
5
+ * `--mode apply --policy deterministic-dedupe` OR the per-request headers `x-compaction-mode: apply` +
6
+ * `x-compaction-policy: deterministic-dedupe`. When the server's configured intent and the request
7
+ * headers CONFLICT, this resolver FAILS CLOSED (falls back to record, records the reason). Default is
8
+ * always record (byte-safe, no mutation).
9
+ */
10
+ import { type ApplyPolicyName } from "./apply-policy.js";
11
+ export type GatewayEffectiveMode = "record" | "apply" | "dry-run";
12
+ export interface ApplyActivation {
13
+ /** The mode the server will actually execute. Forced to "record" whenever we fail closed. */
14
+ mode: GatewayEffectiveMode;
15
+ /** True when apply OR dry-run was requested (by server config or header) — even if we fail closed. */
16
+ requested: boolean;
17
+ /** The resolved policy (only `deterministic-dedupe` is known). */
18
+ policy?: ApplyPolicyName;
19
+ /** Content-free label for the receipt's approval_status / activation source. */
20
+ activation: "record" | "explicit-mode" | "explicit-header" | "dry-run-mode" | "dry-run-header" | "fail-closed";
21
+ /** Set when apply/dry-run was requested but we fell back to record (conflict / unknown policy). */
22
+ failClosedReason?: string;
23
+ }
24
+ /**
25
+ * Resolve the effective apply activation from the server config + request headers. PURE.
26
+ *
27
+ * Precedence + conflict rules (fail closed on any disagreement):
28
+ * - Default server mode `record` carries no mutating intent → a header alone may activate apply/dry-run.
29
+ * - If BOTH the server AND a header express a mutating intent and they DIFFER (mode or policy) → conflict.
30
+ * - An explicit header `record` is an opt-OUT → record (never a conflict).
31
+ * - Apply/dry-run requires the known policy `deterministic-dedupe`; anything else fails closed.
32
+ * - An unknown mode value fails closed.
33
+ */
34
+ export declare function resolveApplyActivation(params: {
35
+ serverMode: string;
36
+ serverPolicy?: string;
37
+ headerMode?: string;
38
+ headerPolicy?: string;
39
+ }): ApplyActivation;