@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
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `compaction precall <tool> [--interactive 0|1] -- <commandParts…>` (PUBLIC CLI — engine-free).
|
|
3
|
+
*
|
|
4
|
+
* The BEFORE-CALL step the persistent Codex/Cursor PATH shim invokes IMMEDIATELY BEFORE the real
|
|
5
|
+
* binary runs (charter 2026-07-04-run-10, extended run-11). It is a FAIL-OPEN, CONTENT-FREE side
|
|
6
|
+
* effect that decides what argv the shim should run:
|
|
7
|
+
*
|
|
8
|
+
* 1. Locate the prompt in the tool's argv (conservative, fail-closed — see `locatePromptArg`) and run
|
|
9
|
+
* the deterministic before-call analysis (`analyzeBeforeCall`) IN-PROCESS.
|
|
10
|
+
* 2. Decide whether an APPROVED mutation is even POSSIBLE via the STRICT whitelist gate
|
|
11
|
+
* `resolveSafeMutation` (run-11). Apply is available ONLY when the prompt is the value of a known
|
|
12
|
+
* value-taking prompt flag AND every other argv token is known — for the REAL Codex/Cursor CLIs
|
|
13
|
+
* (whose prompt is a bare positional) this is NEVER true, so apply is never offered and the ORIGINAL
|
|
14
|
+
* always runs. The apply path below is DORMANT for the real tools by construction.
|
|
15
|
+
* 3. When there is avoidable context:
|
|
16
|
+
* - apply-available + interactive TTY → show the recommendation + `[y] apply · [n] no · [v] view`.
|
|
17
|
+
* `y` → retain the ORIGINAL locally (recoverable) and emit the COMPACTED argv (ONLY the prompt
|
|
18
|
+
* argument replaced). `n`/anything-else/`[v]`-then-decline → emit the ORIGINAL.
|
|
19
|
+
* - apply-NOT-available (real tools) → recommendation + `[v]` details only; emit the ORIGINAL.
|
|
20
|
+
* - no interactive TTY → NEVER ask; emit the ORIGINAL.
|
|
21
|
+
* Record ONE content-free activity event with the honest outcome (applied / declined / not-asked).
|
|
22
|
+
* 4. ALWAYS emit some argv (NUL-delimited) to stdout and exit 0 — the ORIGINAL unless the operator
|
|
23
|
+
* EXPLICITLY approved a whitelist-safe compaction on a TTY.
|
|
24
|
+
*
|
|
25
|
+
* SAFETY (charter run-11, binding): NEVER mutate an ambiguous/unknown/bare-positional form; NEVER
|
|
26
|
+
* without explicit per-invocation approval on a TTY; NO auto-apply / no default yes / no global
|
|
27
|
+
* preference; no-TTY/decline/error/uncertain → ORIGINAL unchanged (fail-closed to original). Every
|
|
28
|
+
* failure mode falls open to the original, so this step can never break or block the wrapped tool.
|
|
29
|
+
* The interaction (and only the interaction) uses `/dev/tty`; stdout carries ONLY the machine argv.
|
|
30
|
+
*/
|
|
31
|
+
import { closeSync, openSync, readFileSync, readSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import { analyzeBeforeCall, locatePromptArg, resolveSafeMutation, resolveStdinPromptBoundary } from "../../core/before-call.js";
|
|
34
|
+
import { planStdinApply } from "../../core/before-call-stdin.js";
|
|
35
|
+
import { buildBeforeCallActivityEvent } from "../../core/before-call-activity.js";
|
|
36
|
+
import { retainOriginalPrompt } from "../../core/before-call-recovery.js";
|
|
37
|
+
import { appendActivityEvent, DEFAULT_ACTIVITY_DIRECTORY } from "../../core/activity-store.js";
|
|
38
|
+
function activityDir(cwd) {
|
|
39
|
+
return cwd ? path.join(cwd, ".compaction", "activity") : DEFAULT_ACTIVITY_DIRECTORY;
|
|
40
|
+
}
|
|
41
|
+
/** Emit the argv NUL-delimited (each element terminated by a NUL) so ANY prompt content is byte-safe. */
|
|
42
|
+
function emitArgv(argv) {
|
|
43
|
+
process.stdout.write(argv.map((a) => `${a}\u0000`).join(""));
|
|
44
|
+
}
|
|
45
|
+
/** Open the controlling terminal for read+write, or return null when there is none (never throws). */
|
|
46
|
+
function openTty() {
|
|
47
|
+
try {
|
|
48
|
+
return openSync("/dev/tty", "r+");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function ttyWrite(fd, text) {
|
|
55
|
+
try {
|
|
56
|
+
writeSync(fd, text);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* best-effort; a display failure must never break the tool */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Read a single line from the tty (blocking, one byte at a time). Empty string on EOF/error. */
|
|
63
|
+
function ttyReadLine(fd) {
|
|
64
|
+
const buf = Buffer.alloc(1);
|
|
65
|
+
let line = "";
|
|
66
|
+
for (;;) {
|
|
67
|
+
let n = 0;
|
|
68
|
+
try {
|
|
69
|
+
n = readSync(fd, buf, 0, 1, null);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (n === 0)
|
|
75
|
+
break;
|
|
76
|
+
const ch = buf.toString("utf8");
|
|
77
|
+
if (ch === "\n")
|
|
78
|
+
break;
|
|
79
|
+
if (ch !== "\r")
|
|
80
|
+
line += ch;
|
|
81
|
+
}
|
|
82
|
+
return line.trim();
|
|
83
|
+
}
|
|
84
|
+
/** Content-free recommendation lines (counts/policy/labels ONLY — never the prompt text). */
|
|
85
|
+
function recommendationLines(rec) {
|
|
86
|
+
return [
|
|
87
|
+
"",
|
|
88
|
+
"compaction before-call recommendation (recommendation-only — nothing is applied automatically):",
|
|
89
|
+
` policy: ${rec.policy}`,
|
|
90
|
+
` input tokens (local-estimate, pre-call): ${rec.input_tokens_before} → ${rec.input_tokens_after_estimate}` +
|
|
91
|
+
` (−${rec.reduction_tokens_estimate})`,
|
|
92
|
+
` evidence: ${rec.evidence_label}`,
|
|
93
|
+
` ${rec.reduction_label}`,
|
|
94
|
+
" This is a local-estimate delta, NOT a saving and NOT provider-reported.",
|
|
95
|
+
" To apply this before-call compaction, run: compaction apply-context <your-trace> --approve-in-workflow-use",
|
|
96
|
+
" Your command will now run UNCHANGED (the original input is passed through).",
|
|
97
|
+
""
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
/** Content-free detail lines for `[v] view` (counts/policy ONLY — never a dump of the prompt). */
|
|
101
|
+
function detailLines(rec) {
|
|
102
|
+
return [
|
|
103
|
+
"",
|
|
104
|
+
" details (content-free — no prompt text is shown or stored):",
|
|
105
|
+
` context blocks in input: ${rec.blocks_total}`,
|
|
106
|
+
` exact-duplicate blocks that would be removed: ${rec.blocks_removed}`,
|
|
107
|
+
` policy: ${rec.policy}`,
|
|
108
|
+
` input tokens (local-estimate): ${rec.input_tokens_before} → ${rec.input_tokens_after_estimate}`,
|
|
109
|
+
""
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
/** Content-free reduction percentage string (e.g. "33.8%"), or "0.0%" when the base is zero. */
|
|
113
|
+
function reductionPercent(rec) {
|
|
114
|
+
if (rec.input_tokens_before <= 0)
|
|
115
|
+
return "0.0%";
|
|
116
|
+
return `${((rec.reduction_tokens_estimate / rec.input_tokens_before) * 100).toFixed(1)}%`;
|
|
117
|
+
}
|
|
118
|
+
/** Content-free recommendation lines for the APPROVED-APPLY path ([y/n/v] — apply is available). */
|
|
119
|
+
function applyRecommendationLines(rec, boundary = "prompt-flag") {
|
|
120
|
+
const whatChanges = boundary === "stdin"
|
|
121
|
+
? " Applying replaces the stdin prompt STREAM with the compacted input; every command-line argument is"
|
|
122
|
+
: " Applying replaces ONLY the prompt argument with the compacted input; every other argument is";
|
|
123
|
+
return [
|
|
124
|
+
"",
|
|
125
|
+
"compaction found avoidable context before this call.",
|
|
126
|
+
` input context: ${rec.input_tokens_before} → ${rec.input_tokens_after_estimate} tokens (−${rec.reduction_tokens_estimate})`,
|
|
127
|
+
` estimated reduction: ${reductionPercent(rec)}`,
|
|
128
|
+
` policy: ${rec.policy}`,
|
|
129
|
+
` evidence: ${rec.evidence_label}`,
|
|
130
|
+
" original retained: yes recoverability: pass",
|
|
131
|
+
" This is a LOCAL-ESTIMATE delta, NOT a saving and NOT provider-reported.",
|
|
132
|
+
`${whatChanges} byte-for-byte unchanged, and the ORIGINAL is retained locally (recoverable). Auto-apply is OFF.`,
|
|
133
|
+
""
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Drive the interactive `[y] apply · [n] no · [v] view details` decision on `io`. FAIL-CLOSED to
|
|
138
|
+
* DECLINE: `y` (and only `y`) approves; `v` shows content-free details then re-prompts; `n`, empty
|
|
139
|
+
* (Enter), EOF, or ANY other input declines (the original runs unchanged). No default yes. This is the
|
|
140
|
+
* function the PTY/`[v]` test drives directly with a fake tty stream (charter run-11 fallback).
|
|
141
|
+
*/
|
|
142
|
+
export function runApplyDecision(io, rec, boundary = "prompt-flag") {
|
|
143
|
+
for (const line of applyRecommendationLines(rec, boundary))
|
|
144
|
+
io.write(`${line}\n`);
|
|
145
|
+
for (;;) {
|
|
146
|
+
io.write(" apply this compaction now? [y] yes · [n] no (run original) · [v] view details: ");
|
|
147
|
+
const answer = io.readLine().trim().toLowerCase();
|
|
148
|
+
if (answer === "y")
|
|
149
|
+
return "apply";
|
|
150
|
+
if (answer === "v") {
|
|
151
|
+
for (const line of detailLines(rec))
|
|
152
|
+
io.write(`${line}\n`);
|
|
153
|
+
continue; // back to the [y/n/v] choice — viewing details never approves
|
|
154
|
+
}
|
|
155
|
+
return "decline"; // n / Enter / EOF / anything else → fail-closed to the original
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Atomically write `content` to `target` (write a sibling `.part`, then rename — no partial reads). */
|
|
159
|
+
function atomicWriteFile(target, content) {
|
|
160
|
+
const tmp = `${target}.part`;
|
|
161
|
+
writeFileSync(tmp, content, "utf8");
|
|
162
|
+
renameSync(tmp, target);
|
|
163
|
+
}
|
|
164
|
+
/** Best-effort one-line note on the controlling terminal (never throws; a display failure is a no-op). */
|
|
165
|
+
function ttyNote(text) {
|
|
166
|
+
const fd = openTty();
|
|
167
|
+
if (fd === null)
|
|
168
|
+
return;
|
|
169
|
+
try {
|
|
170
|
+
ttyWrite(fd, text);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
try {
|
|
174
|
+
closeSync(fd);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* ignore */
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* STDIN-BOUNDARY apply path (charter run-12). The shim buffered the tool's stdin into `stdinFile` and
|
|
183
|
+
* (when an apply is approved) reads the compacted stream back from `compactedOut`. This handler:
|
|
184
|
+
* 1. reads the buffered stdin,
|
|
185
|
+
* 2. plans the decision via `planStdinApply` (safe only for the Codex `exec` stdin boundary; the
|
|
186
|
+
* `[y/n/v]` approval runs on `/dev/tty`),
|
|
187
|
+
* 3. on approval ONLY: retains the ORIGINAL stdin locally FIRST (fail-closed if retention fails), then
|
|
188
|
+
* atomically writes the compacted stdin to `compactedOut` (the shim's apply channel),
|
|
189
|
+
* 4. records ONE content-free activity event with the honest outcome.
|
|
190
|
+
* It emits NOTHING on stdout — the compacted channel is the file. Leaving `compactedOut` empty means the
|
|
191
|
+
* shim feeds the ORIGINAL stdin unchanged (the fail-closed default for every non-approved path).
|
|
192
|
+
*/
|
|
193
|
+
async function handleStdinBoundary(opts) {
|
|
194
|
+
let stdinContent;
|
|
195
|
+
try {
|
|
196
|
+
stdinContent = readFileSync(opts.stdinFile, "utf8");
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return; // cannot read the buffered stdin → do nothing → the shim feeds the original unchanged
|
|
200
|
+
}
|
|
201
|
+
const plan = planStdinApply({
|
|
202
|
+
tool: opts.tool,
|
|
203
|
+
argv: opts.argv,
|
|
204
|
+
stdinContent,
|
|
205
|
+
interactive: opts.interactive,
|
|
206
|
+
decide: (rec) => {
|
|
207
|
+
const fd = openTty();
|
|
208
|
+
if (fd === null)
|
|
209
|
+
return "decline"; // no terminal to ask on → fail-closed to the original
|
|
210
|
+
try {
|
|
211
|
+
const io = { write: (t) => ttyWrite(fd, t), readLine: () => ttyReadLine(fd) };
|
|
212
|
+
return runApplyDecision(io, rec, "stdin");
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
try {
|
|
216
|
+
closeSync(fd);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
/* ignore */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
if (!plan.boundary.safe) {
|
|
225
|
+
// stdin is NOT the prompt boundary (e.g. a positional prompt is present, so Codex would append stdin
|
|
226
|
+
// as a <stdin> block). Preserve the run-10/11 recommendation behavior on the ARGV prompt and feed the
|
|
227
|
+
// ORIGINAL stdin unchanged (emit nothing to compactedOut). stdin is piped here, so the argv
|
|
228
|
+
// recommendation is NON-interactive — exactly the pre-run-12 shim behavior (no new blocking prompt).
|
|
229
|
+
await runArgvPrecall(opts.tool, opts.argv, false, opts.cwd);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
let { approvalStatus, applied } = plan;
|
|
233
|
+
let recoveryPointer;
|
|
234
|
+
let notAvailableReason = plan.notAvailableReason;
|
|
235
|
+
if (plan.emit === "compacted" && plan.compacted !== undefined && !opts.compactedOut) {
|
|
236
|
+
// Approved, but there is NO compacted-out channel to write to (only possible via direct-CLI misuse —
|
|
237
|
+
// the shim always passes --compacted-out) → NOTHING was applied. Record the truth, not applied:true.
|
|
238
|
+
applied = false;
|
|
239
|
+
notAvailableReason = "approved, but no --compacted-out channel — nothing was applied (ran the original unchanged)";
|
|
240
|
+
}
|
|
241
|
+
else if (plan.emit === "compacted" && plan.compacted !== undefined && opts.compactedOut) {
|
|
242
|
+
// Retain the ORIGINAL stdin FIRST — an un-recoverable mutation is NEVER performed (fail-closed).
|
|
243
|
+
try {
|
|
244
|
+
const retained = retainOriginalPrompt(stdinContent, { cwd: opts.cwd });
|
|
245
|
+
atomicWriteFile(opts.compactedOut, plan.compacted); // the shim reads this → feeds the compacted stdin
|
|
246
|
+
recoveryPointer = retained.pointer;
|
|
247
|
+
ttyNote(" applied the compacted stdin (original retained locally, recoverable).\n");
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Retention or the compacted write failed → run the ORIGINAL stdin unchanged; keep compactedOut empty.
|
|
251
|
+
applied = false;
|
|
252
|
+
approvalStatus = "asked-approved";
|
|
253
|
+
notAvailableReason =
|
|
254
|
+
"approved, but retaining the original / writing the compacted stdin failed — ran the original unchanged (fail-closed)";
|
|
255
|
+
try {
|
|
256
|
+
rmSync(`${opts.compactedOut}.part`, { force: true });
|
|
257
|
+
writeFileSync(opts.compactedOut, "");
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
/* the shim treats an empty/missing compacted file as "feed the original" */
|
|
261
|
+
}
|
|
262
|
+
ttyNote(" could not apply safely — running the ORIGINAL stdin unchanged.\n");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (plan.record && plan.recommendation) {
|
|
266
|
+
try {
|
|
267
|
+
const event = buildBeforeCallActivityEvent({
|
|
268
|
+
tool: opts.tool,
|
|
269
|
+
recommendation: plan.recommendation,
|
|
270
|
+
approvalStatus,
|
|
271
|
+
applied,
|
|
272
|
+
...(recoveryPointer ? { recoveryPointer } : {}),
|
|
273
|
+
...(notAvailableReason ? { notAvailableReason } : {}),
|
|
274
|
+
boundary: "stdin"
|
|
275
|
+
});
|
|
276
|
+
await appendActivityEvent(event, activityDir(opts.cwd));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
/* fail-open: activity is a side effect, never a blocker */
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// stdin mode emits NOTHING on stdout — the compacted channel is the file; the shim ignores our stdout.
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The ARGV recommendation / whitelist-apply flow (run-10/run-11): locate the prompt in `argv`, analyze
|
|
286
|
+
* it, and — ONLY for a proven whitelist-safe prompt-flag form explicitly approved on a TTY — return the
|
|
287
|
+
* compacted argv; every other case returns the ORIGINAL argv. Records ONE content-free activity event
|
|
288
|
+
* when there is avoidable context. For the real Codex/Cursor CLIs the apply is dormant (bare-positional
|
|
289
|
+
* prompt), so this is recommendation-only there. Returns the argv the shim should run.
|
|
290
|
+
*/
|
|
291
|
+
async function runArgvPrecall(tool, argv, interactive, cwd) {
|
|
292
|
+
const mutation = resolveSafeMutation(tool, argv);
|
|
293
|
+
// The prompt to analyze: the safe-mutation prompt when apply is available (so the compacted input maps
|
|
294
|
+
// to exactly the token we would rewrite); otherwise the best-effort recommendation locator.
|
|
295
|
+
const promptValue = mutation.applyAvailable ? mutation.originalPrompt : locatePromptArg(tool, argv)?.value;
|
|
296
|
+
if (promptValue === undefined)
|
|
297
|
+
return argv; // no prompt confidently located → original through
|
|
298
|
+
const rec = analyzeBeforeCall(tool, promptValue);
|
|
299
|
+
if (!rec.has_avoidable_context)
|
|
300
|
+
return argv; // honest no-op → original through, no activity noise
|
|
301
|
+
let emitted = argv;
|
|
302
|
+
let approvalStatus = "not-asked";
|
|
303
|
+
let applied = false;
|
|
304
|
+
let recoveryPointer;
|
|
305
|
+
let notAvailableReason = mutation.applyAvailable
|
|
306
|
+
? "no interactive TTY — apply not offered (fail-closed to the original)"
|
|
307
|
+
: mutation.reason;
|
|
308
|
+
if (interactive) {
|
|
309
|
+
const fd = openTty();
|
|
310
|
+
if (fd !== null) {
|
|
311
|
+
try {
|
|
312
|
+
if (mutation.applyAvailable) {
|
|
313
|
+
// APPROVED-APPLY path (dormant for the real tools; only reachable for a proven prompt-flag form).
|
|
314
|
+
const io = { write: (t) => ttyWrite(fd, t), readLine: () => ttyReadLine(fd) };
|
|
315
|
+
const choice = runApplyDecision(io, rec);
|
|
316
|
+
if (choice === "apply") {
|
|
317
|
+
// Retain the ORIGINAL locally FIRST — if retention fails we do NOT apply (fail-closed).
|
|
318
|
+
try {
|
|
319
|
+
const retained = retainOriginalPrompt(mutation.originalPrompt, { cwd });
|
|
320
|
+
emitted = mutation.rebuild(rec.compacted_input);
|
|
321
|
+
approvalStatus = "asked-approved";
|
|
322
|
+
applied = true;
|
|
323
|
+
recoveryPointer = retained.pointer;
|
|
324
|
+
notAvailableReason = undefined;
|
|
325
|
+
ttyWrite(fd, " applied the compacted input (original retained locally, recoverable).\n");
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
emitted = argv;
|
|
329
|
+
approvalStatus = "asked-approved";
|
|
330
|
+
applied = false;
|
|
331
|
+
notAvailableReason = "approved, but retaining the original failed — ran the original unchanged (fail-closed)";
|
|
332
|
+
ttyWrite(fd, " could not retain the original safely — running the ORIGINAL unchanged.\n");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
approvalStatus = "asked-declined";
|
|
337
|
+
notAvailableReason = undefined;
|
|
338
|
+
ttyWrite(fd, " declined — running the ORIGINAL input unchanged.\n");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
// Apply NOT available (the real Codex/Cursor case): recommendation + [v] details only.
|
|
343
|
+
for (const line of recommendationLines(rec))
|
|
344
|
+
ttyWrite(fd, `${line}\n`);
|
|
345
|
+
ttyWrite(fd, " [v] view details · [Enter] continue (run unchanged): ");
|
|
346
|
+
const answer = ttyReadLine(fd).toLowerCase();
|
|
347
|
+
if (answer === "v") {
|
|
348
|
+
for (const line of detailLines(rec))
|
|
349
|
+
ttyWrite(fd, `${line}\n`);
|
|
350
|
+
ttyWrite(fd, " continuing with the ORIGINAL input (apply is not available for this command form).\n");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
try {
|
|
356
|
+
closeSync(fd);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
/* ignore */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const event = buildBeforeCallActivityEvent({
|
|
366
|
+
tool,
|
|
367
|
+
recommendation: rec,
|
|
368
|
+
approvalStatus,
|
|
369
|
+
applied,
|
|
370
|
+
...(recoveryPointer ? { recoveryPointer } : {}),
|
|
371
|
+
...(notAvailableReason ? { notAvailableReason } : {})
|
|
372
|
+
});
|
|
373
|
+
await appendActivityEvent(event, activityDir(cwd));
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
/* fail-open: activity is a side effect, never a blocker */
|
|
377
|
+
}
|
|
378
|
+
return emitted;
|
|
379
|
+
}
|
|
380
|
+
export function registerPrecallCommand(program) {
|
|
381
|
+
program
|
|
382
|
+
.command("precall")
|
|
383
|
+
.description("INTERNAL (used by the Codex/Cursor PATH shims before the real binary runs): observe the input, " +
|
|
384
|
+
"surface a CONTENT-FREE before-call recommendation when it carries avoidable duplicated context, " +
|
|
385
|
+
"record ONE metrics-only activity event, and emit the argv the shim should run. Mutation happens " +
|
|
386
|
+
"ONLY for a whitelist-safe prompt-flag form OR the safe Codex stdin boundary, explicitly approved " +
|
|
387
|
+
"on a TTY; every other case runs the ORIGINAL unchanged. Fail-open, no-TTY → no prompt, no auto-apply.")
|
|
388
|
+
.argument("<tool>", "codex | cursor")
|
|
389
|
+
.option("--interactive <state>", 'Set "1" only when the shim saw an interactive TTY; anything else → non-interactive', "0")
|
|
390
|
+
.option("--stdin-boundary-check", "STDIN-boundary probe (run-12): exit 0 iff stdin is the safe prompt boundary for this argv (reads no stdin)")
|
|
391
|
+
.option("--stdin-file <path>", "STDIN-boundary mode (run-12): the file the shim buffered the tool's stdin into")
|
|
392
|
+
.option("--compacted-out <path>", "STDIN-boundary mode (run-12): where to write the compacted stdin when apply is approved")
|
|
393
|
+
.argument("[commandParts...]", "The tool argv after -- (e.g. -- exec --json \"do X\")")
|
|
394
|
+
.allowUnknownOption(true)
|
|
395
|
+
.action(async (tool, commandPartsRaw, options) => {
|
|
396
|
+
// FAIL-OPEN by contract: on ANY unexpected error, emit the original argv unchanged and exit 0 so
|
|
397
|
+
// the shim can never be broken by this step.
|
|
398
|
+
const argv = commandPartsRaw ?? [];
|
|
399
|
+
try {
|
|
400
|
+
if (tool !== "codex" && tool !== "cursor") {
|
|
401
|
+
emitArgv(argv);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const beforeCallTool = tool;
|
|
405
|
+
// STDIN-BOUNDARY probe (run-12): argv-only, reads NO stdin. Exit 0 iff stdin is the safe prompt
|
|
406
|
+
// boundary; else exit 1. FAIL-SAFE: any error → exit 1 (the shim then does NOT buffer/mutate).
|
|
407
|
+
if (options.stdinBoundaryCheck) {
|
|
408
|
+
try {
|
|
409
|
+
process.exit(resolveStdinPromptBoundary(beforeCallTool, argv).safe ? 0 : 1);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// STDIN-BOUNDARY apply mode (run-12): the shim buffered stdin and wants a decision about the
|
|
416
|
+
// stdin prompt stream. This path emits nothing on stdout (the compacted channel is the file).
|
|
417
|
+
if (options.stdinFile) {
|
|
418
|
+
await handleStdinBoundary({
|
|
419
|
+
tool: beforeCallTool,
|
|
420
|
+
argv,
|
|
421
|
+
interactive: options.interactive === "1",
|
|
422
|
+
stdinFile: options.stdinFile,
|
|
423
|
+
...(options.compactedOut ? { compactedOut: options.compactedOut } : {}),
|
|
424
|
+
cwd: process.cwd()
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// ARGV recommendation / whitelist-apply flow (run-10/run-11). Emits the argv the shim should run.
|
|
429
|
+
emitArgv(await runArgvPrecall(beforeCallTool, argv, options.interactive === "1", process.cwd()));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
emitArgv(argv);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
//# sourceMappingURL=precall.js.map
|
|
@@ -28,7 +28,7 @@ export function registerRecommendCommand(program) {
|
|
|
28
28
|
await runEngineCommand(async () => {
|
|
29
29
|
// Lazy-load the proprietary engine: present in-repo/API, excluded from the public package.
|
|
30
30
|
// When absent, runEngineCommand prints the boundary message and exits cleanly (no stack trace).
|
|
31
|
-
const { applyCompactionPolicy } = await import("../../
|
|
31
|
+
const { applyCompactionPolicy } = await import("../../core/policy-middleware.js");
|
|
32
32
|
const { createRecommendation } = await import("../../engine/recommendation.js");
|
|
33
33
|
const { formatFutureModesNotice, writeRecommendationArtifacts } = await import("../../engine/recommendation-artifacts.js");
|
|
34
34
|
const trace = await parseTraceFile(traceFile);
|