@compaction/cli 0.1.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/LICENSE +663 -0
- package/README.md +319 -0
- package/dist/cli/commands/adapter.d.ts +2 -0
- package/dist/cli/commands/adapter.js +49 -0
- package/dist/cli/commands/aggregate.d.ts +2 -0
- package/dist/cli/commands/aggregate.js +60 -0
- package/dist/cli/commands/analyze.d.ts +2 -0
- package/dist/cli/commands/analyze.js +55 -0
- package/dist/cli/commands/apply-context.d.ts +12 -0
- package/dist/cli/commands/apply-context.js +80 -0
- package/dist/cli/commands/apply.d.ts +2 -0
- package/dist/cli/commands/apply.js +65 -0
- package/dist/cli/commands/approve.d.ts +2 -0
- package/dist/cli/commands/approve.js +55 -0
- package/dist/cli/commands/audit.d.ts +2 -0
- package/dist/cli/commands/audit.js +63 -0
- package/dist/cli/commands/billing-delta.d.ts +2 -0
- package/dist/cli/commands/billing-delta.js +246 -0
- package/dist/cli/commands/capture-claude-code.d.ts +19 -0
- package/dist/cli/commands/capture-claude-code.js +198 -0
- package/dist/cli/commands/capture-provider-usage.d.ts +35 -0
- package/dist/cli/commands/capture-provider-usage.js +113 -0
- package/dist/cli/commands/capture.d.ts +2 -0
- package/dist/cli/commands/capture.js +76 -0
- package/dist/cli/commands/compact.d.ts +2 -0
- package/dist/cli/commands/compact.js +206 -0
- package/dist/cli/commands/eval.d.ts +2 -0
- package/dist/cli/commands/eval.js +66 -0
- package/dist/cli/commands/export-session-seed.d.ts +11 -0
- package/dist/cli/commands/export-session-seed.js +42 -0
- package/dist/cli/commands/feedback.d.ts +2 -0
- package/dist/cli/commands/feedback.js +233 -0
- package/dist/cli/commands/import.d.ts +2 -0
- package/dist/cli/commands/import.js +60 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +216 -0
- package/dist/cli/commands/integrations.d.ts +2 -0
- package/dist/cli/commands/integrations.js +29 -0
- package/dist/cli/commands/launch-treatment-session.d.ts +14 -0
- package/dist/cli/commands/launch-treatment-session.js +50 -0
- package/dist/cli/commands/optimize.d.ts +2 -0
- package/dist/cli/commands/optimize.js +33 -0
- package/dist/cli/commands/recommend.d.ts +4 -0
- package/dist/cli/commands/recommend.js +66 -0
- package/dist/cli/commands/review.d.ts +2 -0
- package/dist/cli/commands/review.js +60 -0
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +49 -0
- package/dist/cli/commands/spend.d.ts +2 -0
- package/dist/cli/commands/spend.js +24 -0
- package/dist/cli/commands/summary.d.ts +2 -0
- package/dist/cli/commands/summary.js +19 -0
- package/dist/cli/engine-degrade.d.ts +40 -0
- package/dist/cli/engine-degrade.js +59 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +59 -0
- package/dist/core/adapter-validation.d.ts +32 -0
- package/dist/core/adapter-validation.js +185 -0
- package/dist/core/adapters/claude-code-adapter.d.ts +51 -0
- package/dist/core/adapters/claude-code-adapter.js +539 -0
- package/dist/core/adapters/claude-code-discovery.d.ts +40 -0
- package/dist/core/adapters/claude-code-discovery.js +100 -0
- package/dist/core/aggregate-format.d.ts +62 -0
- package/dist/core/aggregate-format.js +349 -0
- package/dist/core/api-client/client.d.ts +43 -0
- package/dist/core/api-client/client.js +143 -0
- package/dist/core/api-client/config.d.ts +36 -0
- package/dist/core/api-client/config.js +44 -0
- package/dist/core/api-client/index.d.ts +13 -0
- package/dist/core/api-client/index.js +14 -0
- package/dist/core/api-client/payload.d.ts +93 -0
- package/dist/core/api-client/payload.js +180 -0
- package/dist/core/api-client/types.d.ts +95 -0
- package/dist/core/api-client/types.js +21 -0
- package/dist/core/artifact-writer.d.ts +2 -0
- package/dist/core/artifact-writer.js +14 -0
- package/dist/core/audit-report.d.ts +91 -0
- package/dist/core/audit-report.js +422 -0
- package/dist/core/billing-delta/billing-delta-record.d.ts +290 -0
- package/dist/core/billing-delta/billing-delta-record.js +394 -0
- package/dist/core/capture-adapter.d.ts +74 -0
- package/dist/core/capture-adapter.js +2 -0
- package/dist/core/command-runner.d.ts +29 -0
- package/dist/core/command-runner.js +96 -0
- package/dist/core/content-hash.d.ts +1 -0
- package/dist/core/content-hash.js +5 -0
- package/dist/core/cost-calculator.d.ts +6 -0
- package/dist/core/cost-calculator.js +20 -0
- package/dist/core/feedback-bundle.d.ts +159 -0
- package/dist/core/feedback-bundle.js +406 -0
- package/dist/core/openai-agents-capture.d.ts +66 -0
- package/dist/core/openai-agents-capture.js +475 -0
- package/dist/core/policy-types.d.ts +85 -0
- package/dist/core/policy-types.js +29 -0
- package/dist/core/pricing.d.ts +23 -0
- package/dist/core/pricing.js +55 -0
- package/dist/core/provider-usage/credential-redaction.d.ts +23 -0
- package/dist/core/provider-usage/credential-redaction.js +60 -0
- package/dist/core/provider-usage/provider-usage-adapter.d.ts +65 -0
- package/dist/core/provider-usage/provider-usage-adapter.js +147 -0
- package/dist/core/provider-usage/provider-usage-client.d.ts +89 -0
- package/dist/core/provider-usage/provider-usage-client.js +96 -0
- package/dist/core/report-generator.d.ts +7 -0
- package/dist/core/report-generator.js +275 -0
- package/dist/core/run-aggregator.d.ts +50 -0
- package/dist/core/run-aggregator.js +251 -0
- package/dist/core/run-labels.d.ts +52 -0
- package/dist/core/run-labels.js +80 -0
- package/dist/core/run-trace-converter.d.ts +3 -0
- package/dist/core/run-trace-converter.js +94 -0
- package/dist/core/safety-report.d.ts +52 -0
- package/dist/core/safety-report.js +440 -0
- package/dist/core/savings.d.ts +22 -0
- package/dist/core/savings.js +36 -0
- package/dist/core/session-aggregate.d.ts +211 -0
- package/dist/core/session-aggregate.js +490 -0
- package/dist/core/skill-injection-detector.d.ts +51 -0
- package/dist/core/skill-injection-detector.js +146 -0
- package/dist/core/spend-attribution.d.ts +169 -0
- package/dist/core/spend-attribution.js +611 -0
- package/dist/core/standalone-capsule-provenance.d.ts +4 -0
- package/dist/core/standalone-capsule-provenance.js +62 -0
- package/dist/core/state-capsule.d.ts +3 -0
- package/dist/core/state-capsule.js +147 -0
- package/dist/core/token-accounting.d.ts +62 -0
- package/dist/core/token-accounting.js +81 -0
- package/dist/core/token-estimator.d.ts +3 -0
- package/dist/core/token-estimator.js +18 -0
- package/dist/core/trace-adapters.d.ts +53 -0
- package/dist/core/trace-adapters.js +714 -0
- package/dist/core/trace-fingerprint.d.ts +79 -0
- package/dist/core/trace-fingerprint.js +102 -0
- package/dist/core/trace-intake.d.ts +41 -0
- package/dist/core/trace-intake.js +164 -0
- package/dist/core/trace-parser.d.ts +203 -0
- package/dist/core/trace-parser.js +143 -0
- package/dist/core/types.d.ts +238 -0
- package/dist/core/types.js +2 -0
- package/dist/core/usage-metadata.d.ts +51 -0
- package/dist/core/usage-metadata.js +122 -0
- package/dist/core/waste-detector.d.ts +8 -0
- package/dist/core/waste-detector.js +58 -0
- package/package.json +90 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human renderings (stdout + markdown) and the CONTENT-FREE redacted share bundle for the
|
|
3
|
+
* Track E multi-session aggregate.
|
|
4
|
+
*
|
|
5
|
+
* The share bundle is what a developer hands a founder/admin: the rolled-up aggregate
|
|
6
|
+
* NUMBERS + local labels + evidence tiers, and NOTHING ELSE. It is assembled by WHITELIST
|
|
7
|
+
* from the already-aggregated numeric/enum/short-label fields — there is no code path that
|
|
8
|
+
* copies raw trace text, prompts, completions, tool output, file paths, run ids, or report
|
|
9
|
+
* paths into it. Like the feedback bundle, it is a LOCAL FILE ONLY: no network, no upload.
|
|
10
|
+
*/
|
|
11
|
+
import type { AggregateReport, AggregateTotals, VerificationCounts } from "./session-aggregate.js";
|
|
12
|
+
import { AGGREGATE_EVIDENCE_LABELS } from "./session-aggregate.js";
|
|
13
|
+
import type { RunLabels } from "./run-labels.js";
|
|
14
|
+
export declare const SHARE_BUNDLE_VERSION = "1.0.0";
|
|
15
|
+
export declare const SHARE_NO_UPLOAD_NOTE: string;
|
|
16
|
+
export declare const SHARE_CONTENT_FREE_NOTE: string;
|
|
17
|
+
export declare function formatAggregateReport(report: AggregateReport): string;
|
|
18
|
+
export declare function formatAggregateMarkdown(report: AggregateReport): string;
|
|
19
|
+
export interface ShareBundleBreakdownEntry {
|
|
20
|
+
key: string;
|
|
21
|
+
runs: number;
|
|
22
|
+
input_tokens_saved: number;
|
|
23
|
+
estimated_savings_usd: number;
|
|
24
|
+
ready: number;
|
|
25
|
+
conditional: number;
|
|
26
|
+
not_ready: number;
|
|
27
|
+
}
|
|
28
|
+
export interface AggregateShareBundle {
|
|
29
|
+
share_bundle_version: string;
|
|
30
|
+
generated_at: string;
|
|
31
|
+
privacy: {
|
|
32
|
+
content_free: string;
|
|
33
|
+
upload: string;
|
|
34
|
+
includes: string[];
|
|
35
|
+
excludes: string[];
|
|
36
|
+
};
|
|
37
|
+
/** Local label filters that scoped this rollup (short strings only). */
|
|
38
|
+
applied_filters: RunLabels;
|
|
39
|
+
/** Operator provenance labels (short strings only). */
|
|
40
|
+
rollup_labels: RunLabels;
|
|
41
|
+
session_count: number;
|
|
42
|
+
totals: AggregateTotals;
|
|
43
|
+
verification: VerificationCounts;
|
|
44
|
+
/** Provider/runtime breakdown — short label keys + numbers only (no run ids). */
|
|
45
|
+
by_provider: ShareBundleBreakdownEntry[];
|
|
46
|
+
by_project: ShareBundleBreakdownEntry[];
|
|
47
|
+
by_workflow: ShareBundleBreakdownEntry[];
|
|
48
|
+
evidence_labels: AggregateReport["evidence_labels"];
|
|
49
|
+
limitations: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the CONTENT-FREE aggregate share bundle from an already-aggregated report.
|
|
53
|
+
*
|
|
54
|
+
* WHITELIST construction: this copies ONLY numeric totals, enum verification counts, short
|
|
55
|
+
* label keys/filters, and the static evidence-label/limitation strings. It deliberately
|
|
56
|
+
* does NOT copy `report.sessions[].run_ids`, `skipped_run_directories` (which carry run
|
|
57
|
+
* directory paths), or any per-run identifier — so no run id, report path, or trace content
|
|
58
|
+
* can reach the shared artifact.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildAggregateShareBundle(report: AggregateReport, generatedAt?: string): AggregateShareBundle;
|
|
61
|
+
export declare function formatShareBundleMarkdown(bundle: AggregateShareBundle): string;
|
|
62
|
+
export { AGGREGATE_EVIDENCE_LABELS };
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human renderings (stdout + markdown) and the CONTENT-FREE redacted share bundle for the
|
|
3
|
+
* Track E multi-session aggregate.
|
|
4
|
+
*
|
|
5
|
+
* The share bundle is what a developer hands a founder/admin: the rolled-up aggregate
|
|
6
|
+
* NUMBERS + local labels + evidence tiers, and NOTHING ELSE. It is assembled by WHITELIST
|
|
7
|
+
* from the already-aggregated numeric/enum/short-label fields — there is no code path that
|
|
8
|
+
* copies raw trace text, prompts, completions, tool output, file paths, run ids, or report
|
|
9
|
+
* paths into it. Like the feedback bundle, it is a LOCAL FILE ONLY: no network, no upload.
|
|
10
|
+
*/
|
|
11
|
+
import { AGGREGATE_EVIDENCE_LABELS } from "./session-aggregate.js";
|
|
12
|
+
export const SHARE_BUNDLE_VERSION = "1.0.0";
|
|
13
|
+
export const SHARE_NO_UPLOAD_NOTE = "This is a LOCAL share artifact only. It performs NO network call, NO upload, and NO telemetry. " +
|
|
14
|
+
"It contains aggregate numbers, local labels, and evidence tiers — NOT raw traces, prompts, " +
|
|
15
|
+
"completions, tool output, file contents, run ids, or report paths.";
|
|
16
|
+
export const SHARE_CONTENT_FREE_NOTE = "Content-free by construction: assembled by whitelist from already-aggregated numeric / enum / " +
|
|
17
|
+
"short-label fields. No trace content has a field to land in.";
|
|
18
|
+
function usd(value) {
|
|
19
|
+
return `$${value.toFixed(6)}`;
|
|
20
|
+
}
|
|
21
|
+
function totalsLines(totals) {
|
|
22
|
+
return [
|
|
23
|
+
`runs (compactions): ${totals.compactions}`,
|
|
24
|
+
`token-evidence split: provider-reported ${totals.runs_provider_reported}, local-estimate ${totals.runs_local_estimate}, unknown ${totals.runs_unknown_token_evidence}`,
|
|
25
|
+
`input tokens before: ${totals.total_input_tokens_before}`,
|
|
26
|
+
`input tokens after: ${totals.total_input_tokens_after}`,
|
|
27
|
+
`input tokens saved: ${totals.total_input_tokens_saved}`,
|
|
28
|
+
`output tokens before: ${totals.total_output_tokens_before} (over ${totals.runs_with_output_tokens} run(s); ${totals.runs_without_output_tokens} unknown)`,
|
|
29
|
+
`output tokens after: ${totals.total_output_tokens_after} (over ${totals.runs_with_output_tokens} run(s); ${totals.runs_without_output_tokens} unknown)`,
|
|
30
|
+
`estimated cost before: ${usd(totals.total_estimated_cost_before_usd)} (price-table estimate, NOT billed)`,
|
|
31
|
+
`estimated cost after: ${usd(totals.total_estimated_cost_after_usd)} (price-table estimate, NOT billed)`,
|
|
32
|
+
`estimated savings: ${usd(totals.total_estimated_savings_usd)} (estimated, summed over ${totals.compactions} recorded run(s); NOT billing-confirmed, NOT realized, NOT extrapolated)`
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
function verificationLines(v) {
|
|
36
|
+
return [
|
|
37
|
+
`readiness: ready ${v.ready}, conditional ${v.conditional}, not_ready ${v.not_ready}, not_evaluated ${v.not_evaluated}`,
|
|
38
|
+
`recoverability: pass ${v.recoverability_pass}, fail ${v.recoverability_fail}, not_evaluated ${v.recoverability_not_evaluated}`,
|
|
39
|
+
`commitment-preservation: pass ${v.commitment_pass}, fail ${v.commitment_fail}, not_evaluated ${v.commitment_not_evaluated}`,
|
|
40
|
+
`task-check: pass ${v.task_check_pass}, fail ${v.task_check_fail}, unsupported ${v.task_check_unsupported}, not_evaluated ${v.task_check_not_evaluated}`
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
function filtersLine(filters) {
|
|
44
|
+
const parts = Object.entries(filters).map(([k, val]) => `${k}=${val}`);
|
|
45
|
+
return parts.length === 0 ? "none" : parts.join(", ");
|
|
46
|
+
}
|
|
47
|
+
export function formatAggregateReport(report) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push("compaction multi-session aggregate (LOCAL — no upload, no dashboard)");
|
|
50
|
+
lines.push(`Source: ${report.source_glob}`);
|
|
51
|
+
lines.push(`Generated at: ${report.generated_at}`);
|
|
52
|
+
lines.push(`Applied label filters: ${filtersLine(report.applied_filters)}`);
|
|
53
|
+
lines.push(`Rollup labels: ${filtersLine(report.rollup_labels)}`);
|
|
54
|
+
lines.push(`Sessions: ${report.session_count}`);
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push("Cross-session totals");
|
|
57
|
+
for (const line of totalsLines(report.totals))
|
|
58
|
+
lines.push(`- ${line}`);
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push("Cross-session verification");
|
|
61
|
+
for (const line of verificationLines(report.verification))
|
|
62
|
+
lines.push(`- ${line}`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push("Per-session summary");
|
|
65
|
+
if (report.sessions.length === 0) {
|
|
66
|
+
lines.push("- none (no recorded runs to roll up)");
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
for (const session of report.sessions) {
|
|
70
|
+
lines.push(`- session ${session.session_id}${session.explicit_session_label ? "" : " (run-id fallback)"} — ${session.run_ids.length} run(s)`);
|
|
71
|
+
const labelBits = [];
|
|
72
|
+
if (session.labels.projects.length)
|
|
73
|
+
labelBits.push(`projects: ${session.labels.projects.join("/")}`);
|
|
74
|
+
if (session.labels.workflows.length)
|
|
75
|
+
labelBits.push(`workflows: ${session.labels.workflows.join("/")}`);
|
|
76
|
+
if (session.labels.providers.length)
|
|
77
|
+
labelBits.push(`providers: ${session.labels.providers.join("/")}`);
|
|
78
|
+
if (session.labels.user_labels.length)
|
|
79
|
+
labelBits.push(`labels: ${session.labels.user_labels.join("/")}`);
|
|
80
|
+
if (labelBits.length)
|
|
81
|
+
lines.push(` - ${labelBits.join("; ")}`);
|
|
82
|
+
for (const line of totalsLines(session.totals))
|
|
83
|
+
lines.push(` - ${line}`);
|
|
84
|
+
for (const line of verificationLines(session.verification))
|
|
85
|
+
lines.push(` - ${line}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push("Provider/runtime breakdown (operator-asserted label; 'unlabeled' when absent)");
|
|
90
|
+
appendBreakdownLines(lines, report.by_provider);
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push("Project breakdown");
|
|
93
|
+
appendBreakdownLines(lines, report.by_project);
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("Workflow breakdown");
|
|
96
|
+
appendBreakdownLines(lines, report.by_workflow);
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push("Evidence labels (every figure carries its label)");
|
|
99
|
+
for (const [, label] of Object.entries(report.evidence_labels))
|
|
100
|
+
lines.push(`- ${label}`);
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push("Skipped run directories");
|
|
103
|
+
if (report.skipped_run_directories.length === 0)
|
|
104
|
+
lines.push("- none");
|
|
105
|
+
else
|
|
106
|
+
for (const s of report.skipped_run_directories)
|
|
107
|
+
lines.push(`- ${s.run_directory}: ${s.reason}`);
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push("Limitations");
|
|
110
|
+
for (const limitation of report.limitations)
|
|
111
|
+
lines.push(`- ${limitation}`);
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
function appendBreakdownLines(lines, buckets) {
|
|
115
|
+
if (buckets.length === 0) {
|
|
116
|
+
lines.push("- none");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const bucket of buckets) {
|
|
120
|
+
lines.push(`- ${bucket.key}: ${bucket.totals.compactions} run(s), ${bucket.totals.total_input_tokens_saved} input tokens saved, ${usd(bucket.totals.total_estimated_savings_usd)} estimated savings; readiness ready ${bucket.verification.ready}/conditional ${bucket.verification.conditional}/not_ready ${bucket.verification.not_ready}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function breakdownRows(buckets) {
|
|
124
|
+
if (buckets.length === 0)
|
|
125
|
+
return ["| none | 0 | 0 | $0.000000 | 0 | 0 | 0 |"];
|
|
126
|
+
return buckets.map((b) => `| ${b.key} | ${b.totals.compactions} | ${b.totals.total_input_tokens_saved} | ${usd(b.totals.total_estimated_savings_usd)} | ${b.verification.ready} | ${b.verification.conditional} | ${b.verification.not_ready} |`);
|
|
127
|
+
}
|
|
128
|
+
export function formatAggregateMarkdown(report) {
|
|
129
|
+
const t = report.totals;
|
|
130
|
+
const v = report.verification;
|
|
131
|
+
const lines = [];
|
|
132
|
+
lines.push("# Compaction Multi-Session Aggregate");
|
|
133
|
+
lines.push("");
|
|
134
|
+
lines.push("Local, file-based rollup across recorded sessions/runs. No hosted dashboard, no upload, no telemetry.");
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push(`- Source: ${report.source_glob}`);
|
|
137
|
+
lines.push(`- Generated at: ${report.generated_at}`);
|
|
138
|
+
lines.push(`- Applied label filters: ${filtersLine(report.applied_filters)}`);
|
|
139
|
+
lines.push(`- Rollup labels: ${filtersLine(report.rollup_labels)}`);
|
|
140
|
+
lines.push(`- Sessions: ${report.session_count}`);
|
|
141
|
+
lines.push("");
|
|
142
|
+
lines.push("## Cross-session totals");
|
|
143
|
+
lines.push("");
|
|
144
|
+
lines.push(`- Runs (compactions): ${t.compactions}`);
|
|
145
|
+
lines.push(`- Token-evidence split: provider-reported ${t.runs_provider_reported}, local-estimate ${t.runs_local_estimate}, unknown ${t.runs_unknown_token_evidence}`);
|
|
146
|
+
lines.push(`- Total input tokens before: ${t.total_input_tokens_before}`);
|
|
147
|
+
lines.push(`- Total input tokens after: ${t.total_input_tokens_after}`);
|
|
148
|
+
lines.push(`- Total input tokens saved: ${t.total_input_tokens_saved}`);
|
|
149
|
+
lines.push(`- Total output tokens before: ${t.total_output_tokens_before} (over ${t.runs_with_output_tokens} run(s); ${t.runs_without_output_tokens} unknown)`);
|
|
150
|
+
lines.push(`- Total output tokens after: ${t.total_output_tokens_after} (over ${t.runs_with_output_tokens} run(s); ${t.runs_without_output_tokens} unknown)`);
|
|
151
|
+
lines.push(`- Total estimated cost before: ${usd(t.total_estimated_cost_before_usd)} (price-table estimate, NOT billed)`);
|
|
152
|
+
lines.push(`- Total estimated cost after: ${usd(t.total_estimated_cost_after_usd)} (price-table estimate, NOT billed)`);
|
|
153
|
+
lines.push(`- Total estimated savings: ${usd(t.total_estimated_savings_usd)}`);
|
|
154
|
+
lines.push(`- Savings are estimated and summed over the ${t.compactions} recorded run(s) only; NOT billing-confirmed, NOT realized, NOT extrapolated to any time period.`);
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push("## Cross-session verification");
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(`- Readiness: ready ${v.ready}, conditional ${v.conditional}, not_ready ${v.not_ready}, not_evaluated ${v.not_evaluated}`);
|
|
159
|
+
lines.push(`- Recoverability: pass ${v.recoverability_pass}, fail ${v.recoverability_fail}, not_evaluated ${v.recoverability_not_evaluated}`);
|
|
160
|
+
lines.push(`- Commitment-preservation: pass ${v.commitment_pass}, fail ${v.commitment_fail}, not_evaluated ${v.commitment_not_evaluated}`);
|
|
161
|
+
lines.push(`- Task-check: pass ${v.task_check_pass}, fail ${v.task_check_fail}, unsupported ${v.task_check_unsupported}, not_evaluated ${v.task_check_not_evaluated}`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push("## Per-session summary");
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push("| Session | Runs | Input tokens saved | Estimated savings | ready | conditional | not_ready |");
|
|
166
|
+
lines.push("| --- | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
167
|
+
if (report.sessions.length === 0) {
|
|
168
|
+
lines.push("| none | 0 | 0 | $0.000000 | 0 | 0 | 0 |");
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
for (const s of report.sessions) {
|
|
172
|
+
lines.push(`| ${s.session_id}${s.explicit_session_label ? "" : " (run-id)"} | ${s.run_ids.length} | ${s.totals.total_input_tokens_saved} | ${usd(s.totals.total_estimated_savings_usd)} | ${s.verification.ready} | ${s.verification.conditional} | ${s.verification.not_ready} |`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
lines.push("");
|
|
176
|
+
lines.push("## Provider/runtime breakdown");
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push("Provider/runtime is an operator-asserted local label (never provider-verified); 'unlabeled' when absent.");
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push("| Provider/runtime | Runs | Input tokens saved | Estimated savings | ready | conditional | not_ready |");
|
|
181
|
+
lines.push("| --- | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
182
|
+
for (const row of breakdownRows(report.by_provider))
|
|
183
|
+
lines.push(row);
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push("## Project breakdown");
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push("| Project | Runs | Input tokens saved | Estimated savings | ready | conditional | not_ready |");
|
|
188
|
+
lines.push("| --- | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
189
|
+
for (const row of breakdownRows(report.by_project))
|
|
190
|
+
lines.push(row);
|
|
191
|
+
lines.push("");
|
|
192
|
+
lines.push("## Workflow breakdown");
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("| Workflow | Runs | Input tokens saved | Estimated savings | ready | conditional | not_ready |");
|
|
195
|
+
lines.push("| --- | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
196
|
+
for (const row of breakdownRows(report.by_workflow))
|
|
197
|
+
lines.push(row);
|
|
198
|
+
lines.push("");
|
|
199
|
+
lines.push("## Evidence labels");
|
|
200
|
+
lines.push("");
|
|
201
|
+
for (const [, label] of Object.entries(report.evidence_labels))
|
|
202
|
+
lines.push(`- ${label}`);
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push("## Skipped run directories");
|
|
205
|
+
lines.push("");
|
|
206
|
+
if (report.skipped_run_directories.length === 0) {
|
|
207
|
+
lines.push("- none");
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
for (const s of report.skipped_run_directories)
|
|
211
|
+
lines.push(`- ${s.run_directory}: ${s.reason}`);
|
|
212
|
+
}
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("## Limitations");
|
|
215
|
+
lines.push("");
|
|
216
|
+
for (const limitation of report.limitations)
|
|
217
|
+
lines.push(`- ${limitation}`);
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
const SHARE_INCLUDES = [
|
|
221
|
+
"aggregate token totals (input before/after/saved; output where recorded)",
|
|
222
|
+
"token-evidence split (provider-reported vs local-estimate run counts)",
|
|
223
|
+
"estimated cost + estimated savings totals (price-table estimate; NOT billing-confirmed)",
|
|
224
|
+
"compaction count + session count",
|
|
225
|
+
"verification outcome counts (readiness; recoverability / commitment / task-check pass/fail)",
|
|
226
|
+
"provider/runtime, project, and workflow breakdowns (short label keys + numbers only)",
|
|
227
|
+
"the local label filters / rollup labels (short operator strings)",
|
|
228
|
+
"the per-figure evidence labels and limitations"
|
|
229
|
+
];
|
|
230
|
+
const SHARE_EXCLUDES = [
|
|
231
|
+
"raw trace messages, prompts, completions, tool outputs",
|
|
232
|
+
"source code / file contents / file paths",
|
|
233
|
+
"run ids, report paths, run directories",
|
|
234
|
+
"per-session run id lists",
|
|
235
|
+
"credentials / tokens / environment variables",
|
|
236
|
+
"any free-form text from a run"
|
|
237
|
+
];
|
|
238
|
+
function breakdownEntries(buckets) {
|
|
239
|
+
// WHITELIST: copy ONLY the short label key + numeric aggregates. No run ids, no paths.
|
|
240
|
+
return buckets.map((b) => ({
|
|
241
|
+
key: b.key,
|
|
242
|
+
runs: b.totals.compactions,
|
|
243
|
+
input_tokens_saved: b.totals.total_input_tokens_saved,
|
|
244
|
+
estimated_savings_usd: b.totals.total_estimated_savings_usd,
|
|
245
|
+
ready: b.verification.ready,
|
|
246
|
+
conditional: b.verification.conditional,
|
|
247
|
+
not_ready: b.verification.not_ready
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Build the CONTENT-FREE aggregate share bundle from an already-aggregated report.
|
|
252
|
+
*
|
|
253
|
+
* WHITELIST construction: this copies ONLY numeric totals, enum verification counts, short
|
|
254
|
+
* label keys/filters, and the static evidence-label/limitation strings. It deliberately
|
|
255
|
+
* does NOT copy `report.sessions[].run_ids`, `skipped_run_directories` (which carry run
|
|
256
|
+
* directory paths), or any per-run identifier — so no run id, report path, or trace content
|
|
257
|
+
* can reach the shared artifact.
|
|
258
|
+
*/
|
|
259
|
+
export function buildAggregateShareBundle(report, generatedAt = new Date().toISOString()) {
|
|
260
|
+
return {
|
|
261
|
+
share_bundle_version: SHARE_BUNDLE_VERSION,
|
|
262
|
+
generated_at: generatedAt,
|
|
263
|
+
privacy: {
|
|
264
|
+
content_free: SHARE_CONTENT_FREE_NOTE,
|
|
265
|
+
upload: SHARE_NO_UPLOAD_NOTE,
|
|
266
|
+
includes: SHARE_INCLUDES,
|
|
267
|
+
excludes: SHARE_EXCLUDES
|
|
268
|
+
},
|
|
269
|
+
applied_filters: report.applied_filters,
|
|
270
|
+
rollup_labels: report.rollup_labels,
|
|
271
|
+
session_count: report.session_count,
|
|
272
|
+
totals: report.totals,
|
|
273
|
+
verification: report.verification,
|
|
274
|
+
by_provider: breakdownEntries(report.by_provider),
|
|
275
|
+
by_project: breakdownEntries(report.by_project),
|
|
276
|
+
by_workflow: breakdownEntries(report.by_workflow),
|
|
277
|
+
evidence_labels: report.evidence_labels,
|
|
278
|
+
limitations: report.limitations
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export function formatShareBundleMarkdown(bundle) {
|
|
282
|
+
const t = bundle.totals;
|
|
283
|
+
const v = bundle.verification;
|
|
284
|
+
const lines = [];
|
|
285
|
+
lines.push("# Compaction Aggregate Share Summary (redacted, content-free)");
|
|
286
|
+
lines.push("");
|
|
287
|
+
lines.push("A privacy-safe AGGREGATE summary a developer can hand a founder/admin. It shows value across");
|
|
288
|
+
lines.push("sessions WITHOUT any raw trace text, prompts, completions, tool output, run ids, or paths.");
|
|
289
|
+
lines.push("");
|
|
290
|
+
lines.push(SHARE_CONTENT_FREE_NOTE);
|
|
291
|
+
lines.push("");
|
|
292
|
+
lines.push(SHARE_NO_UPLOAD_NOTE);
|
|
293
|
+
lines.push("");
|
|
294
|
+
lines.push(`- Bundle version: ${bundle.share_bundle_version}`);
|
|
295
|
+
lines.push(`- Generated at: ${bundle.generated_at}`);
|
|
296
|
+
lines.push(`- Applied label filters: ${filtersLine(bundle.applied_filters)}`);
|
|
297
|
+
lines.push(`- Rollup labels: ${filtersLine(bundle.rollup_labels)}`);
|
|
298
|
+
lines.push(`- Sessions: ${bundle.session_count}`);
|
|
299
|
+
lines.push("");
|
|
300
|
+
lines.push("## Aggregate value (estimated, summed over recorded runs)");
|
|
301
|
+
lines.push("");
|
|
302
|
+
lines.push(`- Compactions: ${t.compactions}`);
|
|
303
|
+
lines.push(`- Token-evidence split: provider-reported ${t.runs_provider_reported}, local-estimate ${t.runs_local_estimate}, unknown ${t.runs_unknown_token_evidence}`);
|
|
304
|
+
lines.push(`- Input tokens saved: ${t.total_input_tokens_saved}`);
|
|
305
|
+
lines.push(`- Output tokens before/after: ${t.total_output_tokens_before}/${t.total_output_tokens_after} (over ${t.runs_with_output_tokens} run(s); ${t.runs_without_output_tokens} unknown)`);
|
|
306
|
+
lines.push(`- Estimated cost before/after: ${usd(t.total_estimated_cost_before_usd)} / ${usd(t.total_estimated_cost_after_usd)} (price-table estimate, NOT billed)`);
|
|
307
|
+
lines.push(`- Estimated savings: ${usd(t.total_estimated_savings_usd)} (estimated, summed over ${t.compactions} recorded run(s); NOT billing-confirmed, NOT realized, NOT extrapolated)`);
|
|
308
|
+
lines.push("");
|
|
309
|
+
lines.push("## Verification outcomes");
|
|
310
|
+
lines.push("");
|
|
311
|
+
lines.push(`- Readiness: ready ${v.ready}, conditional ${v.conditional}, not_ready ${v.not_ready}, not_evaluated ${v.not_evaluated}`);
|
|
312
|
+
lines.push(`- Recoverability: pass ${v.recoverability_pass}, fail ${v.recoverability_fail}, not_evaluated ${v.recoverability_not_evaluated}`);
|
|
313
|
+
lines.push(`- Commitment-preservation: pass ${v.commitment_pass}, fail ${v.commitment_fail}, not_evaluated ${v.commitment_not_evaluated}`);
|
|
314
|
+
lines.push(`- Task-check: pass ${v.task_check_pass}, fail ${v.task_check_fail}, unsupported ${v.task_check_unsupported}, not_evaluated ${v.task_check_not_evaluated}`);
|
|
315
|
+
lines.push("");
|
|
316
|
+
lines.push("## Provider/runtime breakdown (operator-asserted label)");
|
|
317
|
+
lines.push("");
|
|
318
|
+
lines.push("| Provider/runtime | Runs | Input tokens saved | Estimated savings | ready | conditional | not_ready |");
|
|
319
|
+
lines.push("| --- | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
320
|
+
if (bundle.by_provider.length === 0)
|
|
321
|
+
lines.push("| none | 0 | 0 | $0.000000 | 0 | 0 | 0 |");
|
|
322
|
+
else
|
|
323
|
+
for (const b of bundle.by_provider) {
|
|
324
|
+
lines.push(`| ${b.key} | ${b.runs} | ${b.input_tokens_saved} | ${usd(b.estimated_savings_usd)} | ${b.ready} | ${b.conditional} | ${b.not_ready} |`);
|
|
325
|
+
}
|
|
326
|
+
lines.push("");
|
|
327
|
+
lines.push("## What this share summary INCLUDES");
|
|
328
|
+
lines.push("");
|
|
329
|
+
for (const item of bundle.privacy.includes)
|
|
330
|
+
lines.push(`- ${item}`);
|
|
331
|
+
lines.push("");
|
|
332
|
+
lines.push("## What it EXCLUDES (never written)");
|
|
333
|
+
lines.push("");
|
|
334
|
+
for (const item of bundle.privacy.excludes)
|
|
335
|
+
lines.push(`- ${item}`);
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push("## Evidence labels");
|
|
338
|
+
lines.push("");
|
|
339
|
+
for (const [, label] of Object.entries(bundle.evidence_labels))
|
|
340
|
+
lines.push(`- ${label}`);
|
|
341
|
+
lines.push("");
|
|
342
|
+
lines.push("## Limitations");
|
|
343
|
+
lines.push("");
|
|
344
|
+
for (const limitation of bundle.limitations)
|
|
345
|
+
lines.push(`- ${limitation}`);
|
|
346
|
+
return lines.join("\n");
|
|
347
|
+
}
|
|
348
|
+
export { AGGREGATE_EVIDENCE_LABELS };
|
|
349
|
+
//# sourceMappingURL=aggregate-format.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction API client — transport (PUBLIC CLI/SDK code).
|
|
3
|
+
*
|
|
4
|
+
* Speaks ONLY the documented HTTP contract (`docs/api/compaction-api-v0.md`). NO engine import,
|
|
5
|
+
* NO auto-upload, NO call at import time. A content-bearing send is gated TWICE: the request
|
|
6
|
+
* body must carry `consent.upload_permitted === true` AND the caller must pass an explicit
|
|
7
|
+
* `confirmed: true` — otherwise `sendRequest` throws BEFORE any network call. Timeout is
|
|
8
|
+
* bounded; content-bearing requests are NEVER auto-retried (re-sending content must be a fresh,
|
|
9
|
+
* explicitly-confirmed action). See `docs/ops/cli-api-client-design.md` §7, §9.
|
|
10
|
+
*/
|
|
11
|
+
import { type ApiConfig } from "./config.js";
|
|
12
|
+
import type { ApiErrorBody, ApiRequestBody, StatusResponse } from "./types.js";
|
|
13
|
+
export declare class ApiNotConfirmedError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class ApiTransportError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
export interface ApiResponse<T> {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
status: number;
|
|
22
|
+
body: T | ApiErrorBody;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `GET /v0/status`. Content-free and idempotent → MAY retry on transient transport errors only.
|
|
26
|
+
* Never retries on a non-2xx HTTP status (a coded error is not transient).
|
|
27
|
+
*/
|
|
28
|
+
export declare function apiStatus(config: ApiConfig): Promise<ApiResponse<StatusResponse>>;
|
|
29
|
+
export interface SendOptions {
|
|
30
|
+
/** Explicit caller confirmation. REQUIRED (in addition to body consent) for content-bearing sends. */
|
|
31
|
+
confirmed?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Send a payload-bearing request to one of the engine-backed endpoints.
|
|
35
|
+
*
|
|
36
|
+
* NO AUTO-UPLOAD: when `payloadClass` is content-bearing, this throws `ApiNotConfirmedError`
|
|
37
|
+
* BEFORE any network call unless BOTH (a) `body.consent.upload_permitted === true` and
|
|
38
|
+
* (b) `options.confirmed === true`. A content-bearing request is NEVER auto-retried.
|
|
39
|
+
*
|
|
40
|
+
* Content-free requests are sent once (the boundary is preserved on the server too, which
|
|
41
|
+
* fail-closes the same way). This function never persists or logs the body or the API key.
|
|
42
|
+
*/
|
|
43
|
+
export declare function sendRequest<T>(config: ApiConfig, path: string, body: ApiRequestBody, options?: SendOptions): Promise<ApiResponse<T>>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction API client — transport (PUBLIC CLI/SDK code).
|
|
3
|
+
*
|
|
4
|
+
* Speaks ONLY the documented HTTP contract (`docs/api/compaction-api-v0.md`). NO engine import,
|
|
5
|
+
* NO auto-upload, NO call at import time. A content-bearing send is gated TWICE: the request
|
|
6
|
+
* body must carry `consent.upload_permitted === true` AND the caller must pass an explicit
|
|
7
|
+
* `confirmed: true` — otherwise `sendRequest` throws BEFORE any network call. Timeout is
|
|
8
|
+
* bounded; content-bearing requests are NEVER auto-retried (re-sending content must be a fresh,
|
|
9
|
+
* explicitly-confirmed action). See `docs/ops/cli-api-client-design.md` §7, §9.
|
|
10
|
+
*/
|
|
11
|
+
import { buildHeaders } from "./config.js";
|
|
12
|
+
import { bodyContainsInlineContent, ConsentError, isContentBearing } from "./payload.js";
|
|
13
|
+
export class ApiNotConfirmedError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "ApiNotConfirmedError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class ApiTransportError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "ApiTransportError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const TRANSIENT_RETRIES = 2;
|
|
26
|
+
const RETRY_BACKOFF_MS = 250;
|
|
27
|
+
function isTransientTransportError(err) {
|
|
28
|
+
// Node fetch wraps connection failures; a timeout is an AbortError (not transient-retryable
|
|
29
|
+
// here because the caller asked for a bounded wait). We treat only connect/reset/DNS-style
|
|
30
|
+
// failures as transient.
|
|
31
|
+
const code = err?.cause?.code;
|
|
32
|
+
return (code === "ECONNREFUSED" ||
|
|
33
|
+
code === "ECONNRESET" ||
|
|
34
|
+
code === "ENOTFOUND" ||
|
|
35
|
+
code === "EAI_AGAIN");
|
|
36
|
+
}
|
|
37
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
40
|
+
try {
|
|
41
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function parseBody(res) {
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
if (text.trim() === "")
|
|
50
|
+
return {};
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(text);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return { error: "invalid_response", details: "non-JSON response body" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `GET /v0/status`. Content-free and idempotent → MAY retry on transient transport errors only.
|
|
60
|
+
* Never retries on a non-2xx HTTP status (a coded error is not transient).
|
|
61
|
+
*/
|
|
62
|
+
export async function apiStatus(config) {
|
|
63
|
+
const url = `${config.url}/v0/status`;
|
|
64
|
+
let lastErr;
|
|
65
|
+
for (let attempt = 0; attempt <= TRANSIENT_RETRIES; attempt++) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetchWithTimeout(url, { method: "GET", headers: buildHeaders(config) }, config.timeoutMs);
|
|
68
|
+
const body = await parseBody(res);
|
|
69
|
+
return { ok: res.ok, status: res.status, body };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
lastErr = err;
|
|
73
|
+
if (!isTransientTransportError(err) || attempt === TRANSIENT_RETRIES)
|
|
74
|
+
break;
|
|
75
|
+
await new Promise((r) => setTimeout(r, RETRY_BACKOFF_MS * (attempt + 1)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new ApiTransportError(`could not reach the Compaction API at ${config.url} (${String(lastErr?.message ?? lastErr)})`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Send a payload-bearing request to one of the engine-backed endpoints.
|
|
82
|
+
*
|
|
83
|
+
* NO AUTO-UPLOAD: when `payloadClass` is content-bearing, this throws `ApiNotConfirmedError`
|
|
84
|
+
* BEFORE any network call unless BOTH (a) `body.consent.upload_permitted === true` and
|
|
85
|
+
* (b) `options.confirmed === true`. A content-bearing request is NEVER auto-retried.
|
|
86
|
+
*
|
|
87
|
+
* Content-free requests are sent once (the boundary is preserved on the server too, which
|
|
88
|
+
* fail-closes the same way). This function never persists or logs the body or the API key.
|
|
89
|
+
*/
|
|
90
|
+
export async function sendRequest(config, path, body, options = {}) {
|
|
91
|
+
const payloadClass = body.payload_class;
|
|
92
|
+
const declaredContentBearing = isContentBearing(payloadClass);
|
|
93
|
+
// DEFENSE-IN-DEPTH, FAIL-CLOSED: the gate is driven by what the body ACTUALLY contains, not by
|
|
94
|
+
// its declared `payload_class`. Because the exported API accepts caller-built bodies, a body can
|
|
95
|
+
// declare a content-free class (`metrics_only`/`redacted_structure`) while still carrying inline
|
|
96
|
+
// raw content in `trace.content` / `original_trace.content` / `compacted_context.content`. We
|
|
97
|
+
// inspect the real body BEFORE any fetch.
|
|
98
|
+
const actuallyCarriesContent = bodyContainsInlineContent(body);
|
|
99
|
+
// A content-free class that ACTUALLY carries inline content is itself an error — reject (the
|
|
100
|
+
// safest option) rather than silently strip-and-send. The label lied about the body.
|
|
101
|
+
if (!declaredContentBearing && actuallyCarriesContent) {
|
|
102
|
+
throw new ConsentError(`consent_required: payload_class "${payloadClass ?? "metrics_only"}" is declared content-free ` +
|
|
103
|
+
"but the body carries inline raw content (trace.content / original_trace.content / " +
|
|
104
|
+
"compacted_context.content). A content-free class must carry zero inline content — refusing to send.");
|
|
105
|
+
}
|
|
106
|
+
// Any body that actually carries inline content (whatever its declared class) requires BOTH
|
|
107
|
+
// explicit upload consent AND an explicit confirm before it may leave the machine.
|
|
108
|
+
if (declaredContentBearing || actuallyCarriesContent) {
|
|
109
|
+
const uploadPermitted = body.consent?.upload_permitted === true;
|
|
110
|
+
if (!uploadPermitted) {
|
|
111
|
+
throw new ApiNotConfirmedError(`refusing to send: body carries inline content but consent.upload_permitted is not true`);
|
|
112
|
+
}
|
|
113
|
+
if (options.confirmed !== true) {
|
|
114
|
+
throw new ApiNotConfirmedError(`refusing to send: content-bearing payload requires an explicit confirm (preview then confirm) before upload`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const contentBearing = declaredContentBearing || actuallyCarriesContent;
|
|
118
|
+
const url = `${config.url}${path}`;
|
|
119
|
+
const init = {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: buildHeaders(config),
|
|
122
|
+
body: JSON.stringify(body)
|
|
123
|
+
};
|
|
124
|
+
// Content-bearing requests are NEVER auto-retried. Content-free requests may retry on
|
|
125
|
+
// transient transport errors only.
|
|
126
|
+
const maxAttempts = contentBearing ? 0 : TRANSIENT_RETRIES;
|
|
127
|
+
let lastErr;
|
|
128
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetchWithTimeout(url, init, config.timeoutMs);
|
|
131
|
+
const parsed = await parseBody(res);
|
|
132
|
+
return { ok: res.ok, status: res.status, body: parsed };
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
lastErr = err;
|
|
136
|
+
if (!isTransientTransportError(err) || attempt === maxAttempts)
|
|
137
|
+
break;
|
|
138
|
+
await new Promise((r) => setTimeout(r, RETRY_BACKOFF_MS * (attempt + 1)));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new ApiTransportError(`could not reach the Compaction API at ${config.url} (${String(lastErr?.message ?? lastErr)})`);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction API client — configuration (PUBLIC CLI/SDK code).
|
|
3
|
+
*
|
|
4
|
+
* Reads env ONLY. Persists NOTHING. The default URL is the LOCAL-DEV server
|
|
5
|
+
* (`127.0.0.1`); there is no remote URL baked into the public package. The API key
|
|
6
|
+
* (when set) is attached as a bearer token at call time and is NEVER persisted, logged,
|
|
7
|
+
* or printed. See `docs/ops/cli-api-client-design.md` §1.
|
|
8
|
+
*/
|
|
9
|
+
/** Local-dev default. Deliberately NOT a remote URL — a fresh checkout talks to a LOCAL server. */
|
|
10
|
+
export declare const DEFAULT_API_URL = "http://127.0.0.1:8787";
|
|
11
|
+
/** Default per-request timeout. Override with `COMPACTION_API_TIMEOUT_MS`. */
|
|
12
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
13
|
+
export interface ApiConfig {
|
|
14
|
+
/** Resolved base URL (no trailing slash). */
|
|
15
|
+
url: string;
|
|
16
|
+
/** Optional bearer token. `undefined` when `COMPACTION_API_KEY` is unset. */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
/** Per-request timeout in milliseconds. */
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
}
|
|
21
|
+
/** A minimal env shape so this is pure/testable (defaults to `process.env`). */
|
|
22
|
+
export type EnvLike = Record<string, string | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve API config from the environment. Pure: same env in → same config out. Never reads
|
|
25
|
+
* or writes any file and never mutates the env.
|
|
26
|
+
*
|
|
27
|
+
* - `COMPACTION_API_URL` → base URL (default LOCAL `127.0.0.1:8787`).
|
|
28
|
+
* - `COMPACTION_API_KEY` → optional bearer token (unset by default).
|
|
29
|
+
* - `COMPACTION_API_TIMEOUT_MS` → optional positive integer timeout.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveApiConfig(env?: EnvLike): ApiConfig;
|
|
32
|
+
/**
|
|
33
|
+
* Build request headers. The API key (when present) is attached as a bearer token. Returns a
|
|
34
|
+
* fresh object each call; the key is never stored elsewhere.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildHeaders(config: ApiConfig): Record<string, string>;
|