@compaction/cli 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -12
- package/dist/cli/commands/activity.d.ts +8 -0
- package/dist/cli/commands/activity.js +35 -0
- package/dist/cli/commands/apply-context.js +68 -0
- package/dist/cli/commands/billing-delta.js +12 -1
- package/dist/cli/commands/capture-claude-code.d.ts +44 -0
- package/dist/cli/commands/capture-claude-code.js +206 -0
- package/dist/cli/commands/capture.js +283 -1
- package/dist/cli/commands/compact.js +24 -11
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.js +130 -0
- 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/import.js +3 -1
- 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 +310 -18
- 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 +39 -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/context-store-eval-cases.d.ts +6 -0
- package/dist/core/context-store-eval-cases.js +331 -0
- package/dist/core/context-store-eval.d.ts +140 -0
- package/dist/core/context-store-eval.js +138 -0
- package/dist/core/context-store-fs.d.ts +73 -0
- package/dist/core/context-store-fs.js +157 -0
- package/dist/core/context-store-sufficiency.d.ts +98 -0
- package/dist/core/context-store-sufficiency.js +135 -0
- package/dist/core/context-store.d.ts +155 -0
- package/dist/core/context-store.js +228 -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/report-generator.d.ts +19 -1
- package/dist/core/report-generator.js +51 -0
- package/dist/core/run-aggregator.d.ts +56 -1
- package/dist/core/run-aggregator.js +93 -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-adapters.d.ts +8 -0
- package/dist/core/trace-adapters.js +4 -0
- package/dist/core/trace-parser.d.ts +13 -13
- package/dist/core/trace-parser.js +6 -6
- package/dist/core/types.d.ts +45 -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,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction Gateway server — RECORD MODE (PUBLIC CLI/SDK code — engine-free). Charter run-15.
|
|
3
|
+
*
|
|
4
|
+
* A LOCAL, byte-safe, OpenAI-compatible reverse proxy: a developer points their OpenAI client's base URL
|
|
5
|
+
* at this gateway; the gateway forwards the request to the real provider BYTE-FOR-BYTE, streams the
|
|
6
|
+
* response back BYTE-FOR-BYTE, and records ONE content-free receipt (`receipt.ts`) with the provider's
|
|
7
|
+
* reported token/cache usage. It NEVER mutates the request or the response (model-visible bytes
|
|
8
|
+
* unchanged), NEVER stores content, and NEVER persists the client's API key.
|
|
9
|
+
*
|
|
10
|
+
* Run-15 implements ONLY `record` mode (no cache-optimization, no apply, no LCM). Built on `node:http`
|
|
11
|
+
* (the repo's server style — see `apps/api/src/server.ts`); zero new dependency.
|
|
12
|
+
*/
|
|
13
|
+
import http from "node:http";
|
|
14
|
+
import https from "node:https";
|
|
15
|
+
import { URL } from "node:url";
|
|
16
|
+
import { buildGatewayReceipt, appendGatewayReceipt, formatFreshBilledInputReduction } from "./receipt.js";
|
|
17
|
+
import { usageFromResponseBody } from "./openai-usage.js";
|
|
18
|
+
import { resolveApplyActivation } from "./apply-activation.js";
|
|
19
|
+
import { planDeterministicDedupe } from "./apply-policy.js";
|
|
20
|
+
import { saveOriginalForRecovery } from "./recovery.js";
|
|
21
|
+
import { buildApplyReceipt } from "./apply-receipt.js";
|
|
22
|
+
/** How much of the RESPONSE tail to keep for usage parsing (bounded — never the whole body). */
|
|
23
|
+
const USAGE_TAIL_BYTES = 64 * 1024;
|
|
24
|
+
/** Cap the buffered REQUEST body (forwarded verbatim). Large enough for any normal chat request. */
|
|
25
|
+
const MAX_REQUEST_BYTES = 25 * 1024 * 1024;
|
|
26
|
+
/** Hop-by-hop headers that must NOT be blindly forwarded (node manages the connection). */
|
|
27
|
+
const HOP_BY_HOP = new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"]);
|
|
28
|
+
/** Best-effort, content-free extraction of the `model` field from a request body (metadata, not content). */
|
|
29
|
+
function requestModel(body) {
|
|
30
|
+
const text = body.toString("utf8").trim();
|
|
31
|
+
if (!text.startsWith("{"))
|
|
32
|
+
return undefined;
|
|
33
|
+
try {
|
|
34
|
+
const obj = JSON.parse(text);
|
|
35
|
+
return typeof obj.model === "string" ? obj.model : undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Read the full request body into a Buffer (byte-exact, for verbatim forwarding). */
|
|
42
|
+
function readRequestBody(req) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let size = 0;
|
|
46
|
+
req.on("data", (c) => {
|
|
47
|
+
size += c.length;
|
|
48
|
+
if (size > MAX_REQUEST_BYTES) {
|
|
49
|
+
reject(new Error("request body exceeds gateway limit"));
|
|
50
|
+
req.destroy();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
chunks.push(c);
|
|
54
|
+
});
|
|
55
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
56
|
+
req.on("error", reject);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create (but do not start) the gateway record-mode server. `createGatewayServer` is exported so tests
|
|
61
|
+
* can bind an ephemeral port. Every request is forwarded byte-for-byte; the receipt append is
|
|
62
|
+
* best-effort and NEVER alters or delays the client's response bytes.
|
|
63
|
+
*/
|
|
64
|
+
export function createGatewayServer(options) {
|
|
65
|
+
if (options.mode !== "record" && options.mode !== "apply" && options.mode !== "dry-run") {
|
|
66
|
+
// The server refuses to pretend to support a mode it does not implement.
|
|
67
|
+
throw new Error(`gateway mode '${options.mode}' is not implemented (record | apply | dry-run)`);
|
|
68
|
+
}
|
|
69
|
+
const log = options.log ?? (() => { });
|
|
70
|
+
const upstreamOrigin = new URL(options.upstream).origin;
|
|
71
|
+
return http.createServer((req, res) => {
|
|
72
|
+
void handleProxy(req, res, options, upstreamOrigin, log);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function handleProxy(req, res, options, upstreamOrigin, log) {
|
|
76
|
+
const endpoint = (req.url ?? "/").split("?")[0];
|
|
77
|
+
// Optional CLIENT-SET proof-run grouping id — READ ONLY (an opaque label, not content). The gateway
|
|
78
|
+
// never injects/mutates the request; this header (when the client sends it) is forwarded to the
|
|
79
|
+
// upstream unchanged AND recorded on the receipt so a manual proof run can pair its receipts.
|
|
80
|
+
const proofHeader = req.headers["x-compaction-proof-run"];
|
|
81
|
+
const proofRunId = typeof proofHeader === "string" && proofHeader.trim() !== "" ? proofHeader : undefined;
|
|
82
|
+
let requestBody;
|
|
83
|
+
try {
|
|
84
|
+
requestBody = await readRequestBody(req);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
if (!res.headersSent)
|
|
88
|
+
res.writeHead(413, { "content-type": "application/json" });
|
|
89
|
+
res.end(JSON.stringify({ error: { message: "compaction gateway: request body too large", type: "gateway_error" } }));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Resolve EXPLICIT apply activation (default record → byte-safe passthrough, exactly as before). Apply
|
|
93
|
+
// requires the server's `--mode apply --policy …` OR the request's x-compaction-mode/-policy headers;
|
|
94
|
+
// a conflict fails closed (record). The ORIGINAL request bytes are the default forwarded body.
|
|
95
|
+
const activation = resolveApplyActivation({
|
|
96
|
+
serverMode: options.mode,
|
|
97
|
+
...(options.policy ? { serverPolicy: options.policy } : {}),
|
|
98
|
+
...(headerValue(req.headers["x-compaction-mode"]) ? { headerMode: headerValue(req.headers["x-compaction-mode"]) } : {}),
|
|
99
|
+
...(headerValue(req.headers["x-compaction-policy"]) ? { headerPolicy: headerValue(req.headers["x-compaction-policy"]) } : {})
|
|
100
|
+
});
|
|
101
|
+
let bodyToForward = requestBody;
|
|
102
|
+
const apply = resolveApplyOutcome(activation, endpoint, req.method, requestBody, options, log);
|
|
103
|
+
if (apply?.applied && apply.mutatedBody)
|
|
104
|
+
bodyToForward = apply.mutatedBody;
|
|
105
|
+
const target = new URL((req.url ?? "/"), upstreamOrigin);
|
|
106
|
+
const client = target.protocol === "http:" ? http : https;
|
|
107
|
+
// Forward headers verbatim EXCEPT host (must be the upstream host), content-length (recomputed from the
|
|
108
|
+
// forwarded body), hop-by-hop headers, and Compaction's own control headers (mode/policy are local-only
|
|
109
|
+
// and are NEVER sent upstream). The client's Authorization rides through untouched and is NEVER read,
|
|
110
|
+
// stored, or logged by the gateway.
|
|
111
|
+
const headers = {};
|
|
112
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
113
|
+
if (v === undefined)
|
|
114
|
+
continue;
|
|
115
|
+
const key = k.toLowerCase();
|
|
116
|
+
if (key === "host" || key === "content-length" || key === "x-compaction-mode" || key === "x-compaction-policy" || HOP_BY_HOP.has(key))
|
|
117
|
+
continue;
|
|
118
|
+
headers[k] = v;
|
|
119
|
+
}
|
|
120
|
+
headers["host"] = target.host;
|
|
121
|
+
if (bodyToForward.length > 0)
|
|
122
|
+
headers["content-length"] = String(bodyToForward.length);
|
|
123
|
+
const upstreamReq = client.request({ protocol: target.protocol, hostname: target.hostname, port: target.port, path: target.pathname + target.search, method: req.method, headers }, (upstreamRes) => {
|
|
124
|
+
// Forward status + headers VERBATIM, then stream the body byte-for-byte to the client.
|
|
125
|
+
res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
126
|
+
upstreamRes.pipe(res); // byte-safe: the client sees the exact upstream bytes (streaming or not)
|
|
127
|
+
// Tee a bounded TAIL of the response for usage parsing (usage rides in the final JSON/SSE chunk).
|
|
128
|
+
// This is a READ-ONLY copy — it never touches the bytes the client receives.
|
|
129
|
+
const tail = [];
|
|
130
|
+
let tailBytes = 0;
|
|
131
|
+
upstreamRes.on("data", (c) => {
|
|
132
|
+
tail.push(c);
|
|
133
|
+
tailBytes += c.length;
|
|
134
|
+
while (tailBytes > USAGE_TAIL_BYTES && tail.length > 1) {
|
|
135
|
+
tailBytes -= tail[0].length;
|
|
136
|
+
tail.shift();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
upstreamRes.on("end", () => {
|
|
140
|
+
const shared = {
|
|
141
|
+
endpoint,
|
|
142
|
+
upstreamStatus: upstreamRes.statusCode ?? 0,
|
|
143
|
+
responseTail: Buffer.concat(tail).toString("utf8"),
|
|
144
|
+
requestModel: requestModel(requestBody), // content-free model label from the ORIGINAL body
|
|
145
|
+
...(proofRunId ? { proofRunId } : {})
|
|
146
|
+
};
|
|
147
|
+
if (activation.requested && apply) {
|
|
148
|
+
void recordApplyReceiptFor({ options, activation, apply, log, ...shared });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
void recordReceipt({ options, log, ...shared });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
upstreamReq.on("error", (err) => {
|
|
156
|
+
// The gateway itself failed to reach upstream — an honest gateway error (never a faked success).
|
|
157
|
+
log(`compaction gateway: upstream error for ${endpoint}: ${err.message}`);
|
|
158
|
+
if (!res.headersSent) {
|
|
159
|
+
res.writeHead(502, { "content-type": "application/json" });
|
|
160
|
+
res.end(JSON.stringify({ error: { message: `compaction gateway: upstream request failed (${err.message})`, type: "gateway_error" } }));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
res.end();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
if (bodyToForward.length > 0)
|
|
167
|
+
upstreamReq.write(bodyToForward);
|
|
168
|
+
upstreamReq.end();
|
|
169
|
+
}
|
|
170
|
+
/** First string value of a (possibly array) header, trimmed to non-empty, else undefined. */
|
|
171
|
+
function headerValue(v) {
|
|
172
|
+
const s = Array.isArray(v) ? v[0] : v;
|
|
173
|
+
return typeof s === "string" && s.trim() !== "" ? s : undefined;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Decide what apply does for this request. NEVER mutates unless: apply mode is active, the shape is
|
|
177
|
+
* supported, a safe duplicate exists, AND the original was successfully retained for recovery FIRST.
|
|
178
|
+
* dry-run always forwards the original. Any uncertainty → fail closed (forward original, record the reason).
|
|
179
|
+
*/
|
|
180
|
+
function resolveApplyOutcome(activation, endpoint, method, originalBody, options, log) {
|
|
181
|
+
if (!activation.requested)
|
|
182
|
+
return null;
|
|
183
|
+
if (activation.failClosedReason)
|
|
184
|
+
return { applied: false, failClosedReason: activation.failClosedReason };
|
|
185
|
+
if (method !== "POST")
|
|
186
|
+
return { applied: false, failClosedReason: `apply supports only POST requests; got ${method ?? "unknown"} — forwarded unchanged` };
|
|
187
|
+
const plan = planDeterministicDedupe(endpoint, originalBody.toString("utf8"));
|
|
188
|
+
if (!plan.supported)
|
|
189
|
+
return { plan, applied: false, ...(plan.failClosedReason ? { failClosedReason: plan.failClosedReason } : {}) };
|
|
190
|
+
if (activation.mode === "dry-run")
|
|
191
|
+
return { plan, applied: false }; // preview only — original forwarded
|
|
192
|
+
if (!plan.changed || !plan.mutatedBody)
|
|
193
|
+
return { plan, applied: false }; // no safe duplicate → forward original
|
|
194
|
+
// APPLY: retain the exact original locally FIRST; only then forward the mutated body. If retention
|
|
195
|
+
// fails we fail closed (never mutate without a recoverable original).
|
|
196
|
+
try {
|
|
197
|
+
const recoveryId = saveOriginalForRecovery(options.cwd ?? process.cwd(), {
|
|
198
|
+
endpoint,
|
|
199
|
+
policy: activation.policy ?? "deterministic-dedupe",
|
|
200
|
+
originalBody: originalBody.toString("utf8")
|
|
201
|
+
});
|
|
202
|
+
return { plan, applied: true, mutatedBody: Buffer.from(plan.mutatedBody, "utf8"), recoveryId };
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
log(`compaction gateway: apply retention failed — forwarding original unchanged (${e.message})`);
|
|
206
|
+
return { plan, applied: false, failClosedReason: `could not retain the original for recovery — forwarded unchanged (${e.message})` };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Build + append the content-free APPLY/dry-run receipt. Best-effort: never affects the client. */
|
|
210
|
+
async function recordApplyReceiptFor(params) {
|
|
211
|
+
try {
|
|
212
|
+
const usage = usageFromResponseBody(params.responseTail);
|
|
213
|
+
const receipt = buildApplyReceipt({
|
|
214
|
+
provider: params.options.provider,
|
|
215
|
+
endpoint: params.endpoint,
|
|
216
|
+
upstreamStatus: params.upstreamStatus,
|
|
217
|
+
usage,
|
|
218
|
+
activation: params.activation,
|
|
219
|
+
applied: params.apply.applied,
|
|
220
|
+
...(params.apply.plan ? { plan: params.apply.plan } : {}),
|
|
221
|
+
...(params.apply.recoveryId ? { recoveryId: params.apply.recoveryId } : {}),
|
|
222
|
+
...(params.apply.failClosedReason ? { failClosedReason: params.apply.failClosedReason } : {}),
|
|
223
|
+
...(params.requestModel ? { requestModel: params.requestModel } : {}),
|
|
224
|
+
...(params.proofRunId ? { proofRunId: params.proofRunId } : {})
|
|
225
|
+
});
|
|
226
|
+
params.options.onReceipt?.(receipt);
|
|
227
|
+
await appendGatewayReceipt(receipt, params.options.cwd ?? process.cwd());
|
|
228
|
+
params.log(`compaction gateway: apply receipt ${receipt.receipt_id.slice(0, 8)}… policy=${receipt.policy} ` +
|
|
229
|
+
`request_mutated=${receipt.request_mutated} ${receipt.recovery_id ? `recovery=${receipt.recovery_id.slice(0, 8)}… ` : ""}` +
|
|
230
|
+
`${receipt.fail_closed_reason ? `(fail-closed: ${receipt.fail_closed_reason}) ` : ""}— response unchanged; content-free.`);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Never let receipt recording break the proxy; the client's response already completed.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Build + append the content-free receipt. Best-effort: a failure here never affects the client. */
|
|
237
|
+
async function recordReceipt(params) {
|
|
238
|
+
try {
|
|
239
|
+
const usage = usageFromResponseBody(params.responseTail);
|
|
240
|
+
const receipt = buildGatewayReceipt({
|
|
241
|
+
provider: params.options.provider,
|
|
242
|
+
endpoint: params.endpoint,
|
|
243
|
+
mode: "record",
|
|
244
|
+
upstreamStatus: params.upstreamStatus,
|
|
245
|
+
usage,
|
|
246
|
+
...(params.requestModel ? { requestModel: params.requestModel } : {}),
|
|
247
|
+
...(params.proofRunId ? { proofRunId: params.proofRunId } : {})
|
|
248
|
+
});
|
|
249
|
+
params.options.onReceipt?.(receipt);
|
|
250
|
+
await appendGatewayReceipt(receipt, params.options.cwd ?? process.cwd());
|
|
251
|
+
const t = receipt.tokens;
|
|
252
|
+
params.log(`compaction gateway: receipt ${receipt.receipt_id.slice(0, 8)}… (${receipt.token_source}) ` +
|
|
253
|
+
`input=${t.prompt_input ?? "-"} cached=${t.cached_input ?? "-"} billed=${t.billed_fresh_input ?? "-"} output=${t.output ?? "-"} ` +
|
|
254
|
+
`· ${formatFreshBilledInputReduction(receipt.fresh_billed_input_reduction)} — content-free, no mutation (model-visible bytes unchanged).`);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Never let receipt recording break the proxy; the client's response already completed.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Start the gateway on `host:port`. Resolves once it is listening. */
|
|
261
|
+
export function startGatewayServer(options) {
|
|
262
|
+
const server = createGatewayServer(options);
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
server.once("error", reject);
|
|
265
|
+
server.listen(options.port, options.host, () => {
|
|
266
|
+
const addr = server.address();
|
|
267
|
+
const port = typeof addr === "object" && addr ? addr.port : options.port;
|
|
268
|
+
resolve({
|
|
269
|
+
server,
|
|
270
|
+
address: { host: options.host, port },
|
|
271
|
+
close: () => new Promise((r) => server.close(() => r()))
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type GatewayReceipt } from "./receipt.js";
|
|
2
|
+
import { type CacheProofSummary } from "./cache-proof.js";
|
|
3
|
+
export declare const GATEWAY_PID_FILE = "gateway.json";
|
|
4
|
+
/** Content-free record of a running gateway (no request/response content). */
|
|
5
|
+
export interface GatewayPidRecord {
|
|
6
|
+
pid: number;
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
upstream: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
mode: string;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function writeGatewayPid(rec: GatewayPidRecord, cwd?: string): void;
|
|
15
|
+
export declare function readGatewayPid(cwd?: string): GatewayPidRecord | null;
|
|
16
|
+
export declare function removeGatewayPid(cwd?: string): void;
|
|
17
|
+
/** True if a process with `pid` is alive (signal 0). EPERM means it exists but is owned by another user. */
|
|
18
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
19
|
+
/** Probe whether something is listening on host:port (TCP connect, short timeout). Never throws. */
|
|
20
|
+
export declare function probeListening(host: string, port: number, timeoutMs?: number): Promise<boolean>;
|
|
21
|
+
/** Read the local-only content-free receipts (each line is a GatewayReceipt). */
|
|
22
|
+
export declare function readReceipts(cwd?: string): GatewayReceipt[];
|
|
23
|
+
export interface GatewayStatus {
|
|
24
|
+
running: boolean;
|
|
25
|
+
pid?: number;
|
|
26
|
+
base?: string;
|
|
27
|
+
provider?: string;
|
|
28
|
+
mode?: string;
|
|
29
|
+
/** Total local receipts (= requests observed through the gateway in this project). */
|
|
30
|
+
receiptsCount: number;
|
|
31
|
+
/** ISO timestamp of the most recent request observed, when any. */
|
|
32
|
+
lastRequestAt?: string;
|
|
33
|
+
/** True when ANY observed request reported provider cached input tokens. */
|
|
34
|
+
cachedObserved: boolean;
|
|
35
|
+
/** When a `since` is given: were any requests observed at/after it (this-session traffic)? */
|
|
36
|
+
observedSince?: boolean;
|
|
37
|
+
/** Content-free summary over the local receipts (best provider-backed fresh/billed input reduction). */
|
|
38
|
+
summary: CacheProofSummary;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the current gateway status: running (pidfile present + process alive + port listening),
|
|
42
|
+
* plus a content-free rollup of the local receipts. When `since` (ISO) is given, `observedSince`
|
|
43
|
+
* reports whether any request was observed at/after it (for "traffic observed this session"). Never throws.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getGatewayStatus(cwd?: string, since?: string): Promise<GatewayStatus>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway lifecycle status + local pidfile (PUBLIC CLI/SDK code — engine-free). Charter run-17.
|
|
3
|
+
*
|
|
4
|
+
* Supports `compaction gateway status|stop` and the guided onboarding. CONTENT-FREE: the pidfile holds
|
|
5
|
+
* only the process id + listen/upstream metadata (host/port/provider/mode/timestamps) — never any
|
|
6
|
+
* request/response content; the receipts it reads are already content-free. Local-only under
|
|
7
|
+
* `<cwd>/.compaction/gateway/` (gitignored).
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
10
|
+
import net from "node:net";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { DEFAULT_GATEWAY_RECEIPTS_DIR, GATEWAY_RECEIPTS_FILE } from "./receipt.js";
|
|
13
|
+
import { summarizeCacheProof } from "./cache-proof.js";
|
|
14
|
+
export const GATEWAY_PID_FILE = "gateway.json";
|
|
15
|
+
function pidPath(cwd) {
|
|
16
|
+
return path.join(cwd, DEFAULT_GATEWAY_RECEIPTS_DIR, GATEWAY_PID_FILE);
|
|
17
|
+
}
|
|
18
|
+
export function writeGatewayPid(rec, cwd = process.cwd()) {
|
|
19
|
+
mkdirSync(path.join(cwd, DEFAULT_GATEWAY_RECEIPTS_DIR), { recursive: true });
|
|
20
|
+
writeFileSync(pidPath(cwd), `${JSON.stringify(rec, null, 2)}\n`, "utf8");
|
|
21
|
+
}
|
|
22
|
+
export function readGatewayPid(cwd = process.cwd()) {
|
|
23
|
+
try {
|
|
24
|
+
const r = JSON.parse(readFileSync(pidPath(cwd), "utf8"));
|
|
25
|
+
return r && typeof r.pid === "number" ? r : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function removeGatewayPid(cwd = process.cwd()) {
|
|
32
|
+
try {
|
|
33
|
+
rmSync(pidPath(cwd), { force: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* best-effort */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** True if a process with `pid` is alive (signal 0). EPERM means it exists but is owned by another user. */
|
|
40
|
+
export function isProcessAlive(pid) {
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, 0);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return e.code === "EPERM";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Probe whether something is listening on host:port (TCP connect, short timeout). Never throws. */
|
|
50
|
+
export function probeListening(host, port, timeoutMs = 400) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const sock = net.connect({ host, port });
|
|
53
|
+
let done = false;
|
|
54
|
+
const finish = (v) => {
|
|
55
|
+
if (done)
|
|
56
|
+
return;
|
|
57
|
+
done = true;
|
|
58
|
+
sock.destroy();
|
|
59
|
+
resolve(v);
|
|
60
|
+
};
|
|
61
|
+
sock.setTimeout(timeoutMs);
|
|
62
|
+
sock.on("connect", () => finish(true));
|
|
63
|
+
sock.on("timeout", () => finish(false));
|
|
64
|
+
sock.on("error", () => finish(false));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Read the local-only content-free receipts (each line is a GatewayReceipt). */
|
|
68
|
+
export function readReceipts(cwd = process.cwd()) {
|
|
69
|
+
const p = path.join(cwd, DEFAULT_GATEWAY_RECEIPTS_DIR, GATEWAY_RECEIPTS_FILE);
|
|
70
|
+
if (!existsSync(p))
|
|
71
|
+
return [];
|
|
72
|
+
return readFileSync(p, "utf8")
|
|
73
|
+
.trim()
|
|
74
|
+
.split("\n")
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.map((l) => JSON.parse(l));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the current gateway status: running (pidfile present + process alive + port listening),
|
|
80
|
+
* plus a content-free rollup of the local receipts. When `since` (ISO) is given, `observedSince`
|
|
81
|
+
* reports whether any request was observed at/after it (for "traffic observed this session"). Never throws.
|
|
82
|
+
*/
|
|
83
|
+
export async function getGatewayStatus(cwd = process.cwd(), since) {
|
|
84
|
+
const rec = readGatewayPid(cwd);
|
|
85
|
+
let running = false;
|
|
86
|
+
let base;
|
|
87
|
+
if (rec) {
|
|
88
|
+
const alive = isProcessAlive(rec.pid);
|
|
89
|
+
const listening = await probeListening(rec.host, rec.port);
|
|
90
|
+
running = alive && listening;
|
|
91
|
+
base = `http://${rec.host}:${rec.port}`;
|
|
92
|
+
}
|
|
93
|
+
const receipts = readReceipts(cwd);
|
|
94
|
+
const times = receipts.map((r) => r.captured_at).filter((t) => typeof t === "string").sort();
|
|
95
|
+
const lastRequestAt = times.length > 0 ? times[times.length - 1] : undefined;
|
|
96
|
+
const cachedObserved = receipts.some((r) => typeof r.tokens?.cached_input === "number" && r.tokens.cached_input > 0);
|
|
97
|
+
const observedSince = since ? receipts.some((r) => typeof r.captured_at === "string" && r.captured_at >= since) : undefined;
|
|
98
|
+
return {
|
|
99
|
+
running,
|
|
100
|
+
...(rec ? { pid: rec.pid, provider: rec.provider, mode: rec.mode } : {}),
|
|
101
|
+
...(base ? { base } : {}),
|
|
102
|
+
receiptsCount: receipts.length,
|
|
103
|
+
...(lastRequestAt ? { lastRequestAt } : {}),
|
|
104
|
+
cachedObserved,
|
|
105
|
+
...(observedSince !== undefined ? { observedSince } : {}),
|
|
106
|
+
summary: summarizeCacheProof(receipts)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ClaudeCodeHookRecord } from "./claude-code-hook-record.js";
|
|
2
|
+
export declare const HOOK_USAGE_AGGREGATE_SCHEMA: "compaction.hook-usage-aggregate.v1";
|
|
3
|
+
/** Load + validate all hook usage records under baseDir (default `.compaction/hooks`). Invalid files skipped. */
|
|
4
|
+
export declare function loadHookUsageRecords(baseDir?: string): Promise<ClaudeCodeHookRecord[]>;
|
|
5
|
+
export interface HookUsageToolAggregate {
|
|
6
|
+
tool: string;
|
|
7
|
+
/** Deduped record count for this tool. */
|
|
8
|
+
events: number;
|
|
9
|
+
/** Sum of input tokens across records that HAVE input; null when none do (never 0-for-missing). */
|
|
10
|
+
inputTokens: number | null;
|
|
11
|
+
outputTokens: number | null;
|
|
12
|
+
/** Reasoning tokens are not separately reported by Claude Code session usage → always null here. */
|
|
13
|
+
reasoningTokens: number | null;
|
|
14
|
+
/** Distinct honest token sources, preserved exactly (e.g. ["provider-reported"]). */
|
|
15
|
+
tokenSources: string[];
|
|
16
|
+
providers: string[];
|
|
17
|
+
models: string[];
|
|
18
|
+
/** Records whose output tokens are present. */
|
|
19
|
+
outputRecorded: number;
|
|
20
|
+
/** Records flagged provider-reported. */
|
|
21
|
+
providerReportedEvents: number;
|
|
22
|
+
/** Records with NO input and NO output token data (usage unavailable). */
|
|
23
|
+
unavailableEvents: number;
|
|
24
|
+
/** Distinct session ids seen. */
|
|
25
|
+
sessions: number;
|
|
26
|
+
firstRecordedAt: string | null;
|
|
27
|
+
lastRecordedAt: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface HookUsageAggregate {
|
|
30
|
+
schema: typeof HOOK_USAGE_AGGREGATE_SCHEMA;
|
|
31
|
+
/** Records loaded before dedup. */
|
|
32
|
+
totalRecords: number;
|
|
33
|
+
/** Records after dedup by dedupKey. */
|
|
34
|
+
dedupedRecords: number;
|
|
35
|
+
/** Optional ISO lower bound applied to recordedAt. */
|
|
36
|
+
since: string | null;
|
|
37
|
+
tools: HookUsageToolAggregate[];
|
|
38
|
+
}
|
|
39
|
+
export interface AggregateOptions {
|
|
40
|
+
/** Only include records with recordedAt >= this ISO timestamp (time-window filter). */
|
|
41
|
+
since?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Aggregate hook records into a content-free source-status view. Dedups by `dedupKey`. Missing token
|
|
45
|
+
* counts stay null. Token source labels preserved exactly. No savings, no content.
|
|
46
|
+
*/
|
|
47
|
+
export declare function aggregateHookUsageRecords(records: ClaudeCodeHookRecord[], options?: AggregateOptions): HookUsageAggregate;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate the content-free Claude Code hook usage records (PUBLIC CLI/SDK code, engine-free).
|
|
3
|
+
* Rolls up the records written by `capture claude-code --from-hook`
|
|
4
|
+
* (`.compaction/hooks/**/records/*.json`) into a CONTENT-FREE source-status view — by tool, by session,
|
|
5
|
+
* and over a time window — mirroring the `/app` source-status model (events / input / output / token
|
|
6
|
+
* sources / providers / models / provider-reported-vs-unavailable). Local-first; no network.
|
|
7
|
+
*
|
|
8
|
+
* Honesty rails (binding):
|
|
9
|
+
* - Content-free in / content-free out: counts, honest source, ids, providers, models, timestamps only.
|
|
10
|
+
* No prompt/completion/message content, no transcript content, no `last_assistant_message`.
|
|
11
|
+
* - Missing usage stays **null / unavailable**, never 0. A tool with no recorded input tokens reports
|
|
12
|
+
* `inputTokens: null`, not 0.
|
|
13
|
+
* - Token source labels are preserved exactly (provider-reported / local-estimate / unknown).
|
|
14
|
+
* - Idempotent: records are deduped by `dedupKey` so the same Stop/session state is counted once.
|
|
15
|
+
* - NO savings claim — this surfaces usage only.
|
|
16
|
+
*/
|
|
17
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { CLAUDE_CODE_HOOK_RECORD_SCHEMA } from "./claude-code-hook-record.js";
|
|
20
|
+
export const HOOK_USAGE_AGGREGATE_SCHEMA = "compaction.hook-usage-aggregate.v1";
|
|
21
|
+
/** A loaded record plus nothing else — we only ever read the content-free fields. */
|
|
22
|
+
function isHookRecord(value) {
|
|
23
|
+
if (typeof value !== "object" || value === null)
|
|
24
|
+
return false;
|
|
25
|
+
const v = value;
|
|
26
|
+
return v.schema === CLAUDE_CODE_HOOK_RECORD_SCHEMA && typeof v.dedupKey === "string" && typeof v.tool === "string";
|
|
27
|
+
}
|
|
28
|
+
/** Recursively find `*.json` files under any `records/` directory below baseDir. */
|
|
29
|
+
async function findRecordFiles(baseDir) {
|
|
30
|
+
const out = [];
|
|
31
|
+
async function walk(dir) {
|
|
32
|
+
let names;
|
|
33
|
+
try {
|
|
34
|
+
names = await readdir(dir);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return; // missing dir → no records (not an error)
|
|
38
|
+
}
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const full = path.join(dir, name);
|
|
41
|
+
let isDir = false;
|
|
42
|
+
try {
|
|
43
|
+
isDir = (await stat(full)).isDirectory();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (isDir) {
|
|
49
|
+
await walk(full);
|
|
50
|
+
}
|
|
51
|
+
else if (name.endsWith(".json") && path.basename(dir) === "records") {
|
|
52
|
+
out.push(full);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
await walk(baseDir);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** Load + validate all hook usage records under baseDir (default `.compaction/hooks`). Invalid files skipped. */
|
|
60
|
+
export async function loadHookUsageRecords(baseDir = ".compaction/hooks") {
|
|
61
|
+
const files = await findRecordFiles(baseDir);
|
|
62
|
+
const records = [];
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
66
|
+
if (isHookRecord(parsed))
|
|
67
|
+
records.push(parsed);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// skip unreadable/invalid record file (never throw — local-first, best-effort)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return records;
|
|
74
|
+
}
|
|
75
|
+
function addToken(acc, value) {
|
|
76
|
+
if (value === null)
|
|
77
|
+
return acc; // missing contributes nothing and never coerces to 0
|
|
78
|
+
return (acc ?? 0) + value;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Aggregate hook records into a content-free source-status view. Dedups by `dedupKey`. Missing token
|
|
82
|
+
* counts stay null. Token source labels preserved exactly. No savings, no content.
|
|
83
|
+
*/
|
|
84
|
+
export function aggregateHookUsageRecords(records, options = {}) {
|
|
85
|
+
const since = options.since ?? null;
|
|
86
|
+
// Apply the time-window filter first, then dedup by dedupKey (records are already deduped at write time;
|
|
87
|
+
// this is defensive). `totalRecords` counts records IN the window so "deduped from N" reads accurately
|
|
88
|
+
// under --since. When `since` is set, a record with a missing/non-string recordedAt is EXCLUDED (a record
|
|
89
|
+
// with no timestamp cannot be proven to fall inside the window).
|
|
90
|
+
let totalRecords = 0;
|
|
91
|
+
const deduped = new Map();
|
|
92
|
+
for (const r of records) {
|
|
93
|
+
if (since !== null && (typeof r.recordedAt !== "string" || r.recordedAt < since))
|
|
94
|
+
continue;
|
|
95
|
+
totalRecords += 1;
|
|
96
|
+
if (!deduped.has(r.dedupKey))
|
|
97
|
+
deduped.set(r.dedupKey, r);
|
|
98
|
+
}
|
|
99
|
+
const byTool = new Map();
|
|
100
|
+
const sessionsByTool = new Map();
|
|
101
|
+
for (const r of deduped.values()) {
|
|
102
|
+
let agg = byTool.get(r.tool);
|
|
103
|
+
if (!agg) {
|
|
104
|
+
agg = {
|
|
105
|
+
tool: r.tool,
|
|
106
|
+
events: 0,
|
|
107
|
+
inputTokens: null,
|
|
108
|
+
outputTokens: null,
|
|
109
|
+
reasoningTokens: null,
|
|
110
|
+
tokenSources: [],
|
|
111
|
+
providers: [],
|
|
112
|
+
models: [],
|
|
113
|
+
outputRecorded: 0,
|
|
114
|
+
providerReportedEvents: 0,
|
|
115
|
+
unavailableEvents: 0,
|
|
116
|
+
sessions: 0,
|
|
117
|
+
firstRecordedAt: null,
|
|
118
|
+
lastRecordedAt: null
|
|
119
|
+
};
|
|
120
|
+
byTool.set(r.tool, agg);
|
|
121
|
+
sessionsByTool.set(r.tool, new Set());
|
|
122
|
+
}
|
|
123
|
+
agg.events += 1;
|
|
124
|
+
agg.inputTokens = addToken(agg.inputTokens, r.inputTokens ?? null);
|
|
125
|
+
agg.outputTokens = addToken(agg.outputTokens, r.outputTokens ?? null);
|
|
126
|
+
if (typeof r.outputTokens === "number")
|
|
127
|
+
agg.outputRecorded += 1;
|
|
128
|
+
if (r.providerReported === true)
|
|
129
|
+
agg.providerReportedEvents += 1;
|
|
130
|
+
if ((r.inputTokens ?? null) === null && (r.outputTokens ?? null) === null)
|
|
131
|
+
agg.unavailableEvents += 1;
|
|
132
|
+
if (r.tokenSource && !agg.tokenSources.includes(r.tokenSource))
|
|
133
|
+
agg.tokenSources.push(r.tokenSource);
|
|
134
|
+
if (r.provider && !agg.providers.includes(r.provider))
|
|
135
|
+
agg.providers.push(r.provider);
|
|
136
|
+
if (r.model && !agg.models.includes(r.model))
|
|
137
|
+
agg.models.push(r.model);
|
|
138
|
+
if (r.sessionId)
|
|
139
|
+
sessionsByTool.get(r.tool).add(r.sessionId);
|
|
140
|
+
if (typeof r.recordedAt === "string") {
|
|
141
|
+
if (agg.firstRecordedAt === null || r.recordedAt < agg.firstRecordedAt)
|
|
142
|
+
agg.firstRecordedAt = r.recordedAt;
|
|
143
|
+
if (agg.lastRecordedAt === null || r.recordedAt > agg.lastRecordedAt)
|
|
144
|
+
agg.lastRecordedAt = r.recordedAt;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const [tool, agg] of byTool) {
|
|
148
|
+
agg.sessions = sessionsByTool.get(tool).size;
|
|
149
|
+
agg.tokenSources.sort();
|
|
150
|
+
agg.providers.sort();
|
|
151
|
+
agg.models.sort();
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
schema: HOOK_USAGE_AGGREGATE_SCHEMA,
|
|
155
|
+
totalRecords,
|
|
156
|
+
dedupedRecords: deduped.size,
|
|
157
|
+
since,
|
|
158
|
+
tools: Array.from(byTool.values()).sort((a, b) => a.tool.localeCompare(b.tool))
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=hook-usage-aggregate.js.map
|