@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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway-native "run a workflow through the gateway" runner (PUBLIC CLI). Charter run-17.1 / run-18A.
|
|
3
|
+
*
|
|
4
|
+
* The public gateway-native entry point is `compaction gateway run -- <command>`. `compaction dev -- …`
|
|
5
|
+
* remains as a hidden compatibility alias (same behavior). Both call `runThroughGateway`.
|
|
6
|
+
*
|
|
7
|
+
* Runs any command with its OpenAI base URL pointed at the local Compaction Gateway, so an
|
|
8
|
+
* OpenAI-compatible app/SDK routes through Compaction with NO manual baseURL copy-paste:
|
|
9
|
+
* compaction gateway run -- npm run dev
|
|
10
|
+
* compaction gateway run -- node app.js
|
|
11
|
+
*
|
|
12
|
+
* Behavior: start (or REUSE) the local gateway → inject the gateway endpoint env into the child (never a
|
|
13
|
+
* key) → run the child with stdio inherited (stdout/stderr/stdin pass through UNCHANGED) → exit with the
|
|
14
|
+
* child's exit code → print a content-free "traffic observed" summary (receipts recorded during this run).
|
|
15
|
+
* It NEVER reads/stores a provider key, NEVER mutates files, and NEVER mutates requests/responses.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { runGatewayStart, parseListen } from "./gateway.js";
|
|
19
|
+
import { getGatewayStatus, readReceipts } from "../../core/gateway/status.js";
|
|
20
|
+
import { summarizeCacheProof } from "../../core/gateway/cache-proof.js";
|
|
21
|
+
import { formatFreshBilledInputReduction } from "../../core/gateway/receipt.js";
|
|
22
|
+
/** The OpenAI-compatible base-url env vars the SDKs read. We set the base only — never a key. */
|
|
23
|
+
function injectionEnv(base) {
|
|
24
|
+
const v1 = `${base}/v1`;
|
|
25
|
+
return { OPENAI_BASE_URL: v1, OPENAI_API_BASE: v1 };
|
|
26
|
+
}
|
|
27
|
+
/** True when SOME provider key is present in the environment (we never read its value). */
|
|
28
|
+
function hasProviderKey(env) {
|
|
29
|
+
return Boolean((env.OPENAI_API_KEY && env.OPENAI_API_KEY.trim()) || (env.OPENAI_KEY && env.OPENAI_KEY.trim()));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run `command` through the local Compaction Gateway. Shared by `gateway run` (the public gateway-native
|
|
33
|
+
* form) and the hidden `dev` compatibility alias — `label` only changes the message prefix + usage hint.
|
|
34
|
+
* Calls `process.exit(code)` with the child's exit code (so it is a terminal action).
|
|
35
|
+
*/
|
|
36
|
+
export async function runThroughGateway(command, options, label = "compaction gateway run") {
|
|
37
|
+
const parts = command ?? [];
|
|
38
|
+
if (parts.length === 0) {
|
|
39
|
+
console.error(`error: no command given. Usage: ${label} -- <command> (e.g. ${label} -- npm run dev)`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
const provider = options.provider ?? "openai";
|
|
45
|
+
const upstream = options.upstream ?? "https://api.openai.com/v1";
|
|
46
|
+
// Reuse a gateway already running in this project, else start our own (ephemeral, quiet).
|
|
47
|
+
const status = await getGatewayStatus(cwd);
|
|
48
|
+
let base;
|
|
49
|
+
let ownGateway = null;
|
|
50
|
+
if (status.running && status.base) {
|
|
51
|
+
base = status.base;
|
|
52
|
+
console.error(`${label}: reusing the running gateway at ${base}`);
|
|
53
|
+
// Honest note: this runner itself always starts in RECORD; if it reuses a gateway the user explicitly
|
|
54
|
+
// started in a mutating mode, say so plainly (it does not silently change that mode).
|
|
55
|
+
if (status.mode && status.mode !== "record") {
|
|
56
|
+
console.error(`${label}: note — the reused gateway is running in '${status.mode}' mode (you started it explicitly); this is not changing that.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const port = options.listen ? parseListen(options.listen).port : 0; // 0 → ephemeral (no port conflicts)
|
|
61
|
+
try {
|
|
62
|
+
const g = await runGatewayStart({ provider, upstream, mode: "record", host: "127.0.0.1", port, cwd, installSignals: false, log: () => { } });
|
|
63
|
+
base = g.base;
|
|
64
|
+
ownGateway = g;
|
|
65
|
+
console.error(`${label}: started a local gateway at ${base} (record mode; byte-safe; content-free receipts)`);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error(`error: could not start a local gateway — ${err.message}`);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const childEnv = { ...process.env, ...injectionEnv(base) };
|
|
74
|
+
console.error(`${label}: routing → OPENAI_BASE_URL=${base}/v1 (your provider key stays on the child; the gateway never stores it)`);
|
|
75
|
+
if (!hasProviderKey(process.env)) {
|
|
76
|
+
console.error(`${label}: Gateway is ready, but the child process still needs its provider API key (e.g. OPENAI_API_KEY). Compaction never injects or stores keys.`);
|
|
77
|
+
}
|
|
78
|
+
// Snapshot receipts so we can report ONLY the traffic observed during this run (content-free).
|
|
79
|
+
const before = new Set(readReceipts(cwd).map((r) => r.receipt_id));
|
|
80
|
+
const child = spawn(parts[0], parts.slice(1), { stdio: "inherit", env: childEnv, cwd });
|
|
81
|
+
const code = await new Promise((resolve) => {
|
|
82
|
+
child.on("exit", (c, signal) => resolve(typeof c === "number" ? c : signal ? 1 : 0));
|
|
83
|
+
child.on("error", (err) => {
|
|
84
|
+
console.error(`${label}: failed to run '${parts[0]}' — ${err.message}`);
|
|
85
|
+
resolve(127);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
if (ownGateway) {
|
|
89
|
+
try {
|
|
90
|
+
await ownGateway.close();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* best-effort */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Content-free traffic summary: only the receipts recorded during this run.
|
|
97
|
+
const observed = readReceipts(cwd).filter((r) => !before.has(r.receipt_id));
|
|
98
|
+
console.error("");
|
|
99
|
+
if (observed.length > 0) {
|
|
100
|
+
const summary = summarizeCacheProof(observed);
|
|
101
|
+
console.error(`${label}: ✓ traffic observed through Compaction — ${observed.length} request(s) this run.`);
|
|
102
|
+
console.error(` best provider-backed fresh/billed input reduction: ${formatFreshBilledInputReduction(summary.bestReduction)}`);
|
|
103
|
+
console.error(" run 'compaction gateway status' for the full content-free rollup.");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error(`${label}: ○ no requests were observed through Compaction this run.`);
|
|
107
|
+
console.error(" If your app has its own OpenAI base URL configured, it may have bypassed OPENAI_BASE_URL.");
|
|
108
|
+
console.error(" Point your client at the injected base URL, or use 'compaction gateway configure' to set it.");
|
|
109
|
+
}
|
|
110
|
+
process.exit(code);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Register the hidden `dev` compatibility alias. The PUBLIC gateway-native form is `gateway run`
|
|
114
|
+
* (registered in gateway.ts); `dev` is kept for backward compatibility and hidden from top-level help.
|
|
115
|
+
*/
|
|
116
|
+
export function registerDevCommand(program) {
|
|
117
|
+
program
|
|
118
|
+
.command("dev")
|
|
119
|
+
.description("Compatibility alias for 'compaction gateway run -- <command>'. Runs a command with its OpenAI base " +
|
|
120
|
+
"URL pointed at the local Compaction Gateway (starts/reuses the gateway, injects the endpoint — never " +
|
|
121
|
+
"a key, streams output unchanged, preserves the exit code, prints a content-free traffic summary).")
|
|
122
|
+
.option("--provider <name>", "Provider (OpenAI-compatible only).", "openai")
|
|
123
|
+
.option("--upstream <url>", "Upstream provider base URL.", "https://api.openai.com/v1")
|
|
124
|
+
.option("--listen <url>", "Local listen address when starting a gateway (default: an ephemeral local port).")
|
|
125
|
+
.argument("[command...]", "The command to run through the gateway (after --), e.g. -- npm run dev")
|
|
126
|
+
.allowUnknownOption(true)
|
|
127
|
+
.action(async (command, options) => {
|
|
128
|
+
await runThroughGateway(command, options, "compaction dev");
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=dev.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type GatewayServerMode } from "../../core/gateway/server.js";
|
|
3
|
+
/** Parse `--listen http://127.0.0.1:8787` (or `127.0.0.1:8787`) into host + port. */
|
|
4
|
+
export declare function parseListen(listen: string): {
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
};
|
|
8
|
+
export interface GatewayStartConfig {
|
|
9
|
+
provider: string;
|
|
10
|
+
upstream: string;
|
|
11
|
+
mode: GatewayServerMode;
|
|
12
|
+
/** Deterministic apply policy (required for apply/dry-run). Only `deterministic-dedupe` is implemented. */
|
|
13
|
+
policy?: string;
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
log?: (line: string) => void;
|
|
18
|
+
/** Install SIGINT/SIGTERM handlers that close the server + remove the pidfile (default true). */
|
|
19
|
+
installSignals?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface RunningGateway {
|
|
22
|
+
base: string;
|
|
23
|
+
close: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start the gateway from a validated config: bind the server, write the content-free pidfile (so
|
|
27
|
+
* `status`/`stop` work), print the honest banner, and install shutdown handlers that remove the pidfile.
|
|
28
|
+
* Reused by both `gateway start` and the guided onboarding. Throws on bind failure (the caller reports it).
|
|
29
|
+
*/
|
|
30
|
+
export declare function runGatewayStart(config: GatewayStartConfig): Promise<RunningGateway>;
|
|
31
|
+
export declare function registerGatewayCommand(program: Command): void;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `compaction gateway start|status|stop` — the local Compaction Gateway (PUBLIC CLI). Charter run-15
|
|
3
|
+
* (record mode) + run-17 (lifecycle + guided onboarding).
|
|
4
|
+
*
|
|
5
|
+
* RECORD MODE only: a byte-safe, OpenAI-compatible local reverse proxy. Point your OpenAI client's base
|
|
6
|
+
* URL at the gateway; it forwards the request + response to the real provider BYTE-FOR-BYTE and records
|
|
7
|
+
* ONE content-free token/cache/cost receipt per call. No mutation, no cache optimization, no apply, no
|
|
8
|
+
* LCM. The client's API key rides straight through to the provider and is NEVER read, stored, or logged.
|
|
9
|
+
*
|
|
10
|
+
* `runGatewayStart` is exported so the guided onboarding TUI (`GatewayTui` → `init`) can start the same
|
|
11
|
+
* gateway with a config picked interactively — no duplicate server, no new behavior.
|
|
12
|
+
*/
|
|
13
|
+
import { URL } from "node:url";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { writeFileSync } from "node:fs";
|
|
16
|
+
import { startGatewayServer } from "../../core/gateway/server.js";
|
|
17
|
+
import { DEFAULT_GATEWAY_RECEIPTS_DIR, GATEWAY_RECEIPTS_FILE, formatFreshBilledInputReduction } from "../../core/gateway/receipt.js";
|
|
18
|
+
import { writeGatewayPid, removeGatewayPid, readGatewayPid, getGatewayStatus, isProcessAlive } from "../../core/gateway/status.js";
|
|
19
|
+
import { planGatewayConfigure, applyGatewayConfigure, formatConfigurePlan } from "../../core/gateway/configure.js";
|
|
20
|
+
import { DEDUPE_POLICY } from "../../core/gateway/apply-policy.js";
|
|
21
|
+
import { GATEWAY_RECOVERY_DIR, readRecovery } from "../../core/gateway/recovery.js";
|
|
22
|
+
import { runThroughGateway } from "./dev.js";
|
|
23
|
+
/** Parse `--listen http://127.0.0.1:8787` (or `127.0.0.1:8787`) into host + port. */
|
|
24
|
+
export function parseListen(listen) {
|
|
25
|
+
const raw = listen.includes("://") ? listen : `http://${listen}`;
|
|
26
|
+
const u = new URL(raw);
|
|
27
|
+
const port = u.port ? Number.parseInt(u.port, 10) : 8787;
|
|
28
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
29
|
+
throw new Error(`invalid --listen port in '${listen}'`);
|
|
30
|
+
}
|
|
31
|
+
return { host: u.hostname || "127.0.0.1", port };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Start the gateway from a validated config: bind the server, write the content-free pidfile (so
|
|
35
|
+
* `status`/`stop` work), print the honest banner, and install shutdown handlers that remove the pidfile.
|
|
36
|
+
* Reused by both `gateway start` and the guided onboarding. Throws on bind failure (the caller reports it).
|
|
37
|
+
*/
|
|
38
|
+
export async function runGatewayStart(config) {
|
|
39
|
+
if (config.mode !== "record" && config.mode !== "apply" && config.mode !== "dry-run") {
|
|
40
|
+
throw new Error(`gateway mode '${config.mode}' is not implemented (record | apply | dry-run)`);
|
|
41
|
+
}
|
|
42
|
+
const mutating = config.mode === "apply" || config.mode === "dry-run";
|
|
43
|
+
if (mutating && config.policy !== DEDUPE_POLICY) {
|
|
44
|
+
throw new Error(`gateway --mode ${config.mode} requires --policy ${DEDUPE_POLICY} (the only deterministic policy implemented)`);
|
|
45
|
+
}
|
|
46
|
+
const cwd = config.cwd ?? process.cwd();
|
|
47
|
+
const log = config.log ?? ((line) => console.log(line));
|
|
48
|
+
const started = await startGatewayServer({
|
|
49
|
+
provider: config.provider,
|
|
50
|
+
upstream: config.upstream,
|
|
51
|
+
mode: config.mode,
|
|
52
|
+
...(config.policy ? { policy: config.policy } : {}),
|
|
53
|
+
cwd,
|
|
54
|
+
log,
|
|
55
|
+
host: config.host,
|
|
56
|
+
port: config.port
|
|
57
|
+
});
|
|
58
|
+
const base = `http://${started.address.host}:${started.address.port}`;
|
|
59
|
+
writeGatewayPid({
|
|
60
|
+
pid: process.pid,
|
|
61
|
+
host: started.address.host,
|
|
62
|
+
port: started.address.port,
|
|
63
|
+
upstream: config.upstream,
|
|
64
|
+
provider: config.provider,
|
|
65
|
+
mode: config.mode,
|
|
66
|
+
startedAt: new Date().toISOString()
|
|
67
|
+
}, cwd);
|
|
68
|
+
const receiptsPath = path.join(cwd, DEFAULT_GATEWAY_RECEIPTS_DIR, GATEWAY_RECEIPTS_FILE);
|
|
69
|
+
if (config.mode === "record") {
|
|
70
|
+
log("compaction gateway — RECORD mode (byte-safe; no mutation; content-free receipts)");
|
|
71
|
+
}
|
|
72
|
+
else if (config.mode === "dry-run") {
|
|
73
|
+
log(`compaction gateway — DRY-RUN mode (policy ${config.policy}; computes a candidate but forwards the ORIGINAL request unchanged)`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
log(`compaction gateway — APPLY mode · EXPERIMENTAL (policy ${config.policy}; DETERMINISTIC only)`);
|
|
77
|
+
log(" Apply changes model-visible input for KNOWN-SAFE shapes only; the original is retained locally;");
|
|
78
|
+
log(" unknown/complex shapes FAIL CLOSED (forwarded unchanged). System/developer instructions & tool schemas are never touched.");
|
|
79
|
+
}
|
|
80
|
+
log(` provider: ${config.provider} → upstream ${config.upstream}`);
|
|
81
|
+
log(` receipts: ${receiptsPath} (local-only, gitignored; token/cache counts only — no prompt/response content)`);
|
|
82
|
+
if (mutating)
|
|
83
|
+
log(` recovery: ${path.join(cwd, GATEWAY_RECOVERY_DIR)} (local-only; the ORIGINAL request body when apply changes it — recover with 'compaction gateway recover <id>')`);
|
|
84
|
+
log("");
|
|
85
|
+
log(`Gateway running at ${base}`);
|
|
86
|
+
log("");
|
|
87
|
+
log("Run one command through Compaction:");
|
|
88
|
+
log(" compaction gateway run -- npm run dev");
|
|
89
|
+
log("");
|
|
90
|
+
log("Inspect traffic:");
|
|
91
|
+
log(" compaction gateway status");
|
|
92
|
+
log("");
|
|
93
|
+
log("If manual setup is needed:");
|
|
94
|
+
log(` compaction gateway configure (or point your OpenAI client at baseURL = ${base}/v1 — keep your key on the client; the gateway never stores it)`);
|
|
95
|
+
log("");
|
|
96
|
+
log(" Stop from another terminal: compaction gateway stop · or press Ctrl-C here.");
|
|
97
|
+
const close = async () => {
|
|
98
|
+
removeGatewayPid(cwd);
|
|
99
|
+
await started.close();
|
|
100
|
+
};
|
|
101
|
+
if (config.installSignals !== false) {
|
|
102
|
+
const shutdown = async () => {
|
|
103
|
+
await close();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
};
|
|
106
|
+
process.on("SIGINT", () => void shutdown());
|
|
107
|
+
process.on("SIGTERM", () => void shutdown());
|
|
108
|
+
}
|
|
109
|
+
return { base, close };
|
|
110
|
+
}
|
|
111
|
+
export function registerGatewayCommand(program) {
|
|
112
|
+
const gateway = program
|
|
113
|
+
.command("gateway")
|
|
114
|
+
.description("Local Compaction Gateway — a byte-safe, OpenAI-compatible reverse proxy that records a content-free " +
|
|
115
|
+
"token/cache/cost receipt per call. Default RECORD mode (no mutation); EXPERIMENTAL deterministic " +
|
|
116
|
+
"apply/dry-run modes are explicit opt-in (no cache-opt, no LCM, no semantic compaction).");
|
|
117
|
+
gateway
|
|
118
|
+
.command("start")
|
|
119
|
+
.description("Start the local gateway. Point your OpenAI client's base URL at --listen. RECORD forwards byte-for-byte; APPLY (experimental, explicit) deterministically dedupes known-safe shapes and retains the original locally.")
|
|
120
|
+
.option("--provider <name>", "Provider (OpenAI-compatible only).", "openai")
|
|
121
|
+
.option("--upstream <url>", "Upstream provider base URL (the request path is forwarded to its origin).", "https://api.openai.com/v1")
|
|
122
|
+
.option("--listen <url>", "Local address to listen on (e.g. http://127.0.0.1:8787).", "http://127.0.0.1:8787")
|
|
123
|
+
.option("--mode <mode>", "Gateway mode: record (default, byte-safe) | apply (experimental, deterministic) | dry-run (preview, forwards original).", "record")
|
|
124
|
+
.option("--policy <name>", `Deterministic apply policy (required for apply/dry-run). Only '${DEDUPE_POLICY}' is implemented.`)
|
|
125
|
+
.option("--approval <mode>", "Apply approval model (informational in run-17.2: starting apply mode IS the explicit approval; per-request interactive approval is deferred).", "ask")
|
|
126
|
+
.action(async (options) => {
|
|
127
|
+
const mode = (options.mode ?? "record");
|
|
128
|
+
if (mode !== "record" && mode !== "apply" && mode !== "dry-run") {
|
|
129
|
+
console.error(`error: gateway mode '${mode}' is not implemented (record | apply | dry-run).`);
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if ((mode === "apply" || mode === "dry-run") && options.policy !== DEDUPE_POLICY) {
|
|
134
|
+
console.error(`error: --mode ${mode} requires --policy ${DEDUPE_POLICY} (the only deterministic policy implemented). ` +
|
|
135
|
+
`Apply is deterministic-only — no semantic compaction, no LCM.`);
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const provider = options.provider ?? "openai";
|
|
140
|
+
const upstream = options.upstream ?? "https://api.openai.com/v1";
|
|
141
|
+
try {
|
|
142
|
+
new URL(upstream);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
console.error(`error: --upstream '${upstream}' is not a valid URL.`);
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let listen;
|
|
150
|
+
try {
|
|
151
|
+
listen = parseListen(options.listen ?? "http://127.0.0.1:8787");
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.error(`error: ${err.message}`);
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await runGatewayStart({ provider, upstream, mode, ...(options.policy ? { policy: options.policy } : {}), host: listen.host, port: listen.port });
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.error(`error: could not start the gateway on ${listen.host}:${listen.port} — ${err.message}`);
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
gateway
|
|
167
|
+
.command("status")
|
|
168
|
+
.description("Show whether the local gateway is running + a content-free rollup of its receipts (token/cache; no content).")
|
|
169
|
+
.option("--json", "Print the content-free status as JSON.")
|
|
170
|
+
.action(async (options) => {
|
|
171
|
+
const status = await getGatewayStatus(process.cwd());
|
|
172
|
+
if (options.json) {
|
|
173
|
+
console.log(JSON.stringify(status, null, 2));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.log("compaction gateway status");
|
|
177
|
+
if (status.running) {
|
|
178
|
+
console.log(` gateway running: yes ${status.base ?? ""} (pid ${status.pid})`);
|
|
179
|
+
console.log(` provider / mode: ${status.provider} / ${status.mode}`);
|
|
180
|
+
console.log(` listen URL: ${status.base ?? "(unknown)"}`);
|
|
181
|
+
}
|
|
182
|
+
else if (status.pid) {
|
|
183
|
+
console.log(` gateway running: no (stale pidfile for pid ${status.pid} — run 'compaction gateway stop' to clear it)`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(" gateway running: no (start it with 'compaction gateway start', or 'compaction' → Gateway setup)");
|
|
187
|
+
}
|
|
188
|
+
console.log(` requests observed: ${status.receiptsCount} (local-only, content-free)`);
|
|
189
|
+
console.log(` last request: ${status.lastRequestAt ?? "none observed yet"}`);
|
|
190
|
+
console.log(` cached input seen: ${status.receiptsCount > 0 ? (status.cachedObserved ? "yes" : "no") : "unknown (no requests yet)"}`);
|
|
191
|
+
if (status.receiptsCount > 0) {
|
|
192
|
+
console.log(` best fresh/billed input reduction: ${formatFreshBilledInputReduction(status.summary.bestReduction)}`);
|
|
193
|
+
console.log(` ${status.summary.modelVisibleBytesUnchanged ? "model-visible bytes unchanged" : "model-visible bytes: mixed — check receipts"}`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
gateway
|
|
197
|
+
.command("run")
|
|
198
|
+
.description("Run a command through the local Compaction Gateway. Usage: compaction gateway run -- <command>. Starts/reuses the gateway, injects the endpoint into the child (never a key), streams its output unchanged, preserves its exit code, and prints a content-free traffic summary.")
|
|
199
|
+
.option("--provider <name>", "Provider (OpenAI-compatible only).", "openai")
|
|
200
|
+
.option("--upstream <url>", "Upstream provider base URL.", "https://api.openai.com/v1")
|
|
201
|
+
.option("--listen <url>", "Local listen address when starting a gateway (default: an ephemeral local port).")
|
|
202
|
+
.argument("[command...]", "The command to run through the gateway (after --), e.g. -- npm run dev")
|
|
203
|
+
.allowUnknownOption(true)
|
|
204
|
+
.action(async (command, options) => {
|
|
205
|
+
await runThroughGateway(command, options, "compaction gateway run");
|
|
206
|
+
});
|
|
207
|
+
gateway
|
|
208
|
+
.command("activate")
|
|
209
|
+
.description("Print shell export lines that point THIS shell's OpenAI base URL at the local gateway. Use: eval \"$(compaction gateway activate)\". It only PRINTS — it never edits files or mutates your shell, and it never attaches to a running process.")
|
|
210
|
+
.option("--listen <url>", "Gateway listen address to point at (default: the running gateway, else http://127.0.0.1:8787).")
|
|
211
|
+
.action(async (options) => {
|
|
212
|
+
const status = await getGatewayStatus(process.cwd());
|
|
213
|
+
let base;
|
|
214
|
+
if (options.listen) {
|
|
215
|
+
const l = parseListen(options.listen);
|
|
216
|
+
base = `http://${l.host}:${l.port}`;
|
|
217
|
+
}
|
|
218
|
+
else if (status.running && status.base) {
|
|
219
|
+
base = status.base;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
base = "http://127.0.0.1:8787";
|
|
223
|
+
}
|
|
224
|
+
const v1 = `${base}/v1`;
|
|
225
|
+
// Guidance to STDERR (not captured by eval); the exports go to STDOUT (captured by eval).
|
|
226
|
+
console.error('# compaction gateway activate — apply to this shell with: eval "$(compaction gateway activate)"');
|
|
227
|
+
if (!(status.running && status.base) && !options.listen) {
|
|
228
|
+
console.error("# note: no running gateway detected; start one with 'compaction gateway start' (these exports point at the default local address).");
|
|
229
|
+
}
|
|
230
|
+
console.error("# your provider API key stays in your shell — the gateway never reads or stores it.");
|
|
231
|
+
console.log(`export OPENAI_BASE_URL=${v1}`);
|
|
232
|
+
console.log(`export OPENAI_API_BASE=${v1}`);
|
|
233
|
+
});
|
|
234
|
+
gateway
|
|
235
|
+
.command("configure")
|
|
236
|
+
.description("Propose pointing this project's OpenAI base URL at the local gateway (approval-gated: shows a diff; writes nothing without --apply; backs up first; never overwrites an existing base URL without --force).")
|
|
237
|
+
.option("--listen <url>", "Gateway listen address to route to (default: http://127.0.0.1:8787).", "http://127.0.0.1:8787")
|
|
238
|
+
.option("--apply", "Actually write the change (creates a .bak backup first).")
|
|
239
|
+
.option("--force", "Overwrite an existing provider base URL (only with --apply).")
|
|
240
|
+
.action((options) => {
|
|
241
|
+
let listen;
|
|
242
|
+
try {
|
|
243
|
+
listen = parseListen(options.listen ?? "http://127.0.0.1:8787");
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.error(`error: ${err.message}`);
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const baseUrl = `http://${listen.host}:${listen.port}/v1`;
|
|
251
|
+
const cwd = process.cwd();
|
|
252
|
+
const plan = planGatewayConfigure(cwd, baseUrl);
|
|
253
|
+
for (const line of formatConfigurePlan(plan))
|
|
254
|
+
console.log(line);
|
|
255
|
+
if (!options.apply)
|
|
256
|
+
return;
|
|
257
|
+
const result = applyGatewayConfigure(cwd, plan, options.force ?? false);
|
|
258
|
+
console.log("");
|
|
259
|
+
if (result.wrote) {
|
|
260
|
+
console.log(`compaction gateway configure: wrote ${result.targetFile}${result.backupFile ? ` (backup: ${result.backupFile})` : ""}.`);
|
|
261
|
+
console.log(" restart your app so it picks up the new base URL, or run: compaction gateway run -- <command>");
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
console.log(`compaction gateway configure: not written — ${result.reason}`);
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
gateway
|
|
269
|
+
.command("recover")
|
|
270
|
+
.description("Recover the ORIGINAL request body that apply mode changed (retained locally under .compaction/gateway/recovery). Prints to stdout or writes to --out.")
|
|
271
|
+
.argument("<recovery_id>", "The recovery_id shown on the apply receipt.")
|
|
272
|
+
.option("--out <file>", "Write the original body to this file instead of stdout.")
|
|
273
|
+
.action((recoveryId, options) => {
|
|
274
|
+
const rec = readRecovery(process.cwd(), recoveryId);
|
|
275
|
+
if (!rec) {
|
|
276
|
+
console.error(`compaction gateway recover: no retained original found for id '${recoveryId}' under ${GATEWAY_RECOVERY_DIR}.`);
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (options.out) {
|
|
281
|
+
writeFileSync(options.out, rec.original_body, "utf8");
|
|
282
|
+
console.error(`compaction gateway recover: wrote the original request body (${rec.endpoint}, policy ${rec.policy}, captured ${rec.captured_at}) to ${options.out}.`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// The original body IS request content — it prints to stdout only on explicit recovery, never stored in a receipt.
|
|
286
|
+
process.stdout.write(rec.original_body.endsWith("\n") ? rec.original_body : `${rec.original_body}\n`);
|
|
287
|
+
});
|
|
288
|
+
gateway
|
|
289
|
+
.command("stop")
|
|
290
|
+
.description("Stop a running local gateway (started in another terminal) via its pidfile.")
|
|
291
|
+
.action(() => {
|
|
292
|
+
const rec = readGatewayPid(process.cwd());
|
|
293
|
+
if (!rec) {
|
|
294
|
+
console.log("compaction gateway stop: no gateway pidfile found — nothing to stop.");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!isProcessAlive(rec.pid)) {
|
|
298
|
+
removeGatewayPid(process.cwd());
|
|
299
|
+
console.log(`compaction gateway stop: pid ${rec.pid} is not running — cleared the stale pidfile.`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
process.kill(rec.pid, "SIGTERM");
|
|
304
|
+
console.log(`compaction gateway stop: sent SIGTERM to pid ${rec.pid} (${rec.host}:${rec.port}). It removes its own pidfile on exit.`);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
console.error(`compaction gateway stop: could not signal pid ${rec.pid} — ${err.message}`);
|
|
308
|
+
process.exitCode = 1;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=gateway.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type HookUsageAggregate } from "../../core/hook-usage-aggregate.js";
|
|
3
|
+
export declare function registerHooksCommand(program: Command): void;
|
|
4
|
+
/** Render the content-free source-status aggregate as a readable block (provider-reported vs unavailable). */
|
|
5
|
+
export declare function printHookUsageAggregate(aggregate: HookUsageAggregate): string;
|