costlayers 0.8.20 → 0.8.27
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 +17 -8
- package/bin/agentspend.js +292 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# CostLayers CLI
|
|
2
2
|
|
|
3
|
-
CostLayers is
|
|
3
|
+
CostLayers is an AI spend control layer for coding-agent teams. It starts with a free audit, finds repeated context waste, gives API users invoice-mode savings, and gives ChatGPT-login Codex users a usage-stretch meter that shows how much repeated context was avoided.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Run a free AI spend audit:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
cd your-repo
|
|
11
|
-
npx -y https://costlayers.com/costlayers-0.8.
|
|
11
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz audit --monthly-spend 10000
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Use your real monthly AI spend if you have it. Omit `--monthly-spend` to get a repo-only audit. The audit writes `.agentspend/ai-spend-audit.md` and labels scan estimates separately from verified invoice savings. After the audit, `costlayers dashboard` shows the local result even before signup.
|
|
15
15
|
|
|
16
16
|
Run a one-command API savings test:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
export OPENAI_API_KEY=sk-proj-...
|
|
20
|
-
npx -y https://costlayers.com/costlayers-0.8.
|
|
20
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz test --email you@example.com
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
@@ -25,7 +25,13 @@ npx -y https://costlayers.com/costlayers-0.8.20.tgz test --email you@example.com
|
|
|
25
25
|
Inside a repo:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npx -y https://costlayers.com/costlayers-0.8.
|
|
28
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz audit --monthly-spend 10000
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then run Codex with CostLayers:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz codex --email you@example.com --chatgpt
|
|
29
35
|
```
|
|
30
36
|
|
|
31
37
|
This gives Codex `.agentspend/repo-pack.md` and `.agentspend/runtime-plan.md`
|
|
@@ -45,7 +51,7 @@ API write permission:
|
|
|
45
51
|
|
|
46
52
|
```bash
|
|
47
53
|
export OPENAI_API_KEY=sk-proj-...
|
|
48
|
-
npx -y https://costlayers.com/costlayers-0.8.
|
|
54
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz codex --email you@example.com --api
|
|
49
55
|
```
|
|
50
56
|
|
|
51
57
|
ChatGPT-login Codex can be metered, but it does not create per-request OpenAI
|
|
@@ -56,12 +62,13 @@ Platform invoice savings because it is not billed through your Platform API key.
|
|
|
56
62
|
- ChatGPT-login Codex: use `costlayers codex --email you@example.com --chatgpt` to reduce repeated repo context and stretch usage limits.
|
|
57
63
|
- OpenAI Platform API billing: set `OPENAI_API_KEY`, then use `costlayers codex --email you@example.com --api` for invoice-backed savings.
|
|
58
64
|
- Savings proof: set `OPENAI_API_KEY`, then run `costlayers test --email you@example.com`.
|
|
65
|
+
- Free audit: run `costlayers audit --monthly-spend <usd>` before changing a workflow.
|
|
59
66
|
- Other OpenAI-compatible clients: point the client at the CostLayers gateway URL and check `costlayers gateway report`.
|
|
60
67
|
|
|
61
68
|
To install only the Codex profile after signup:
|
|
62
69
|
|
|
63
70
|
```bash
|
|
64
|
-
npx -y https://costlayers.com/costlayers-0.8.
|
|
71
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz codex-profile
|
|
65
72
|
codex --profile costlayers
|
|
66
73
|
```
|
|
67
74
|
|
|
@@ -93,6 +100,8 @@ The hosted reducer defaults to quality-safe reduction:
|
|
|
93
100
|
|
|
94
101
|
Output:
|
|
95
102
|
|
|
103
|
+
- `.agentspend/ai-spend-audit.md`
|
|
104
|
+
- `.agentspend/ai-spend-audit.json`
|
|
96
105
|
- `.agentspend/repo-pack.md`
|
|
97
106
|
- `.agentspend/savings-report.md`
|
|
98
107
|
- `.agentspend/savings-report.json`
|
package/bin/agentspend.js
CHANGED
|
@@ -11,14 +11,14 @@ const { spawnSync } = require("child_process");
|
|
|
11
11
|
|
|
12
12
|
function packageVersion() {
|
|
13
13
|
try {
|
|
14
|
-
return require(path.join(__dirname, "..", "package.json")).version || "0.8.
|
|
14
|
+
return require(path.join(__dirname, "..", "package.json")).version || "0.8.27";
|
|
15
15
|
} catch (_err) {
|
|
16
|
-
return "0.8.
|
|
16
|
+
return "0.8.27";
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const VERSION = packageVersion();
|
|
21
|
-
const INSTALL_SPEC =
|
|
21
|
+
const INSTALL_SPEC = process.env.COSTLAYERS_INSTALL_SPEC || `https://costlayers.com/costlayers-${VERSION}.tgz`;
|
|
22
22
|
const DEFAULT_RUNS_PER_WEEK = 20;
|
|
23
23
|
const WEEKS_PER_MONTH = 4.33;
|
|
24
24
|
const DEFAULT_EXCLUDES = new Set([
|
|
@@ -60,6 +60,7 @@ function usage(exitCode = 0) {
|
|
|
60
60
|
CostLayers ${VERSION}
|
|
61
61
|
|
|
62
62
|
Usage:
|
|
63
|
+
costlayers audit [--email <email>] [--monthly-spend <usd>] [--usage-file <path>]
|
|
63
64
|
costlayers codex [--email <email>] [--chatgpt|--api] [-- <codex args>]
|
|
64
65
|
costlayers test [--email <email>] [--prompt <text>]
|
|
65
66
|
costlayers init [--repo <path>]
|
|
@@ -76,6 +77,7 @@ Usage:
|
|
|
76
77
|
costlayers doctor
|
|
77
78
|
|
|
78
79
|
Commands:
|
|
80
|
+
audit Run a free AI spend audit: repo context waste plus optional monthly spend/imported usage.
|
|
79
81
|
codex Start Codex with CostLayers. Defaults to ChatGPT-login mode unless --api is passed.
|
|
80
82
|
test Run a safe read-only API invoice-mode Codex task and print the CostLayers savings report.
|
|
81
83
|
init Create .agentspend config and agent instructions.
|
|
@@ -195,7 +197,7 @@ function repoConnectionPath(repo) {
|
|
|
195
197
|
return path.join(repo, ".agentspend", "connection.json");
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
function
|
|
200
|
+
function ensureCostLayersGitignore(outDir) {
|
|
199
201
|
ensureDir(outDir);
|
|
200
202
|
const file = path.join(outDir, ".gitignore");
|
|
201
203
|
const required = ["connection.json", "*.secret.json", "gateway-key.txt", "local-cache.json"];
|
|
@@ -239,7 +241,7 @@ function writePrivateJson(file, payload) {
|
|
|
239
241
|
function saveConnection(repo, connection) {
|
|
240
242
|
const outDir = path.join(repo, ".agentspend");
|
|
241
243
|
ensureDir(outDir);
|
|
242
|
-
|
|
244
|
+
ensureCostLayersGitignore(outDir);
|
|
243
245
|
const connectionId = connectionIdFor(repo, connection);
|
|
244
246
|
const full = {
|
|
245
247
|
...connection,
|
|
@@ -677,7 +679,7 @@ function writeLocalCache(outDir, payload) {
|
|
|
677
679
|
function scanToFiles(repo, args) {
|
|
678
680
|
const outDir = path.join(repo, ".agentspend");
|
|
679
681
|
ensureDir(outDir);
|
|
680
|
-
|
|
682
|
+
ensureCostLayersGitignore(outDir);
|
|
681
683
|
const tasks = Number(args.tasks || 100);
|
|
682
684
|
const runsPerWeek = Number(args["runs-per-week"] || DEFAULT_RUNS_PER_WEEK);
|
|
683
685
|
const pricePer1m = Number(args["price-per-1m"] || 2.0);
|
|
@@ -786,7 +788,7 @@ This public scanner estimates repeated context waste. Real savings should be val
|
|
|
786
788
|
function init(repo, options = {}) {
|
|
787
789
|
const outDir = path.join(repo, ".agentspend");
|
|
788
790
|
ensureDir(outDir);
|
|
789
|
-
|
|
791
|
+
ensureCostLayersGitignore(outDir);
|
|
790
792
|
writeIfMissing(path.join(outDir, "config.json"), JSON.stringify({
|
|
791
793
|
version: VERSION,
|
|
792
794
|
created_utc: new Date().toISOString(),
|
|
@@ -862,6 +864,58 @@ function loadConnection(repo, args) {
|
|
|
862
864
|
};
|
|
863
865
|
}
|
|
864
866
|
|
|
867
|
+
function localAudit(repo) {
|
|
868
|
+
const auditPath = path.join(repo, ".agentspend", "ai-spend-audit.json");
|
|
869
|
+
const savingsPath = path.join(repo, ".agentspend", "savings-report.json");
|
|
870
|
+
const auditReportPath = path.join(repo, ".agentspend", "ai-spend-audit.md");
|
|
871
|
+
const savingsReportPath = path.join(repo, ".agentspend", "savings-report.md");
|
|
872
|
+
const audit = readJsonIfExists(auditPath);
|
|
873
|
+
const savings = readJsonIfExists(savingsPath);
|
|
874
|
+
if (!audit && !savings) return null;
|
|
875
|
+
return { audit, savings, auditReportPath, savingsReportPath };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function printLocalDashboard(repo) {
|
|
879
|
+
const local = localAudit(repo);
|
|
880
|
+
if (!local) {
|
|
881
|
+
process.stderr.write([
|
|
882
|
+
"No CostLayers dashboard is available for this repo yet.",
|
|
883
|
+
"",
|
|
884
|
+
"Run a free local audit first:",
|
|
885
|
+
` npx -y ${INSTALL_SPEC} audit --monthly-spend 10000`,
|
|
886
|
+
"",
|
|
887
|
+
"Or create a hosted dashboard:",
|
|
888
|
+
` npx -y ${INSTALL_SPEC} signup --email you@example.com`,
|
|
889
|
+
""
|
|
890
|
+
].join("\n"));
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
const audit = local.audit || {};
|
|
894
|
+
const savings = local.savings || {};
|
|
895
|
+
const contextTokens = audit.tokens_avoided_per_run ?? savings.tokens_avoided_per_repeated_task ?? 0;
|
|
896
|
+
const contextReduction = audit.context_reduction_percent ?? savings.estimated_reduction_percent ?? 0;
|
|
897
|
+
const contextPackTokens = audit.context_pack_tokens ?? savings.context_pack_tokens ?? 0;
|
|
898
|
+
const sourceTokens = audit.source_tokens_indexed ?? savings.source_tokens_indexed ?? 0;
|
|
899
|
+
process.stdout.write(`CostLayers Local Dashboard\n`);
|
|
900
|
+
process.stdout.write(`status: local audit only; no hosted account connected\n`);
|
|
901
|
+
process.stdout.write(`repo: ${path.basename(repo)}\n`);
|
|
902
|
+
if (audit.spend_analyzed_display) process.stdout.write(`ai_spend_analyzed: ${audit.spend_analyzed_display}\n`);
|
|
903
|
+
if (audit.waste_found_display) process.stdout.write(`candidate_waste_found: ${audit.waste_found_display}\n`);
|
|
904
|
+
if (audit.safe_savings_display) process.stdout.write(`quality_safe_savings_now: ${audit.safe_savings_display}\n`);
|
|
905
|
+
process.stdout.write(`context_tokens_avoided_per_agent_run: ${formatInt(contextTokens)}\n`);
|
|
906
|
+
process.stdout.write(`context_reduction: ${Number(contextReduction || 0).toFixed(2)}%\n`);
|
|
907
|
+
process.stdout.write(`before_after_context: ${formatInt(sourceTokens)} -> ${formatInt(contextPackTokens)} tokens\n`);
|
|
908
|
+
process.stdout.write(`local_report: ${local.audit ? local.auditReportPath : local.savingsReportPath}\n`);
|
|
909
|
+
process.stdout.write(`\nNext:\n`);
|
|
910
|
+
process.stdout.write(` Open the local report above, or create the hosted savings meter:\n`);
|
|
911
|
+
process.stdout.write(` npx -y ${INSTALL_SPEC} signup --email you@example.com\n`);
|
|
912
|
+
process.stdout.write(`\nEvidence labels:\n`);
|
|
913
|
+
process.stdout.write(` Context estimate: local scan, visible immediately.\n`);
|
|
914
|
+
process.stdout.write(` Usage stretch: appears after Codex profile token events.\n`);
|
|
915
|
+
process.stdout.write(` API invoice savings: appears only after API-billed traffic is routed through CostLayers.\n`);
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
|
|
865
919
|
function defaultPublicGatewayUrl(engineUrl, apiKey) {
|
|
866
920
|
try {
|
|
867
921
|
const url = new URL(engineUrl);
|
|
@@ -920,10 +974,20 @@ function dashboardUrlFromConnection(connection) {
|
|
|
920
974
|
return (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
|
|
921
975
|
}
|
|
922
976
|
|
|
923
|
-
function printSavingsSummary(report) {
|
|
977
|
+
function printSavingsSummary(report, options = {}) {
|
|
924
978
|
const verdict = savingsVerdict(report);
|
|
925
979
|
const projection = verdict.projection;
|
|
926
980
|
const avoided = verdict.avoided;
|
|
981
|
+
if (options.compact) {
|
|
982
|
+
if (!verdict.meaningful) {
|
|
983
|
+
process.stdout.write(`CostLayers: repo scanned; no meaningful savings claim for this small/simple repo.\n`);
|
|
984
|
+
} else {
|
|
985
|
+
process.stdout.write(`CostLayers found context waste: ${formatInt(report.tokens_avoided_per_repeated_task)} tokens/run avoided (${report.estimated_reduction_percent}% less).\n`);
|
|
986
|
+
process.stdout.write(`Usage-stretch estimate: ${formatUsd(projection.monthlyUsd)}/month at ${formatInt(projection.runsPerWeek)} agent runs/week.\n`);
|
|
987
|
+
}
|
|
988
|
+
process.stdout.write(`Invoice savings require API mode; ChatGPT-login mode keeps native Codex auth.\n`);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
927
991
|
if (!verdict.meaningful) {
|
|
928
992
|
process.stdout.write(`\nCostLayers installed. This repo is too small for a meaningful savings claim.\n`);
|
|
929
993
|
} else {
|
|
@@ -951,6 +1015,177 @@ function printSavingsSummary(report) {
|
|
|
951
1015
|
}
|
|
952
1016
|
}
|
|
953
1017
|
|
|
1018
|
+
function moneyNumber(value) {
|
|
1019
|
+
const text = String(value || "").replace(/[$,_\s]/g, "");
|
|
1020
|
+
const number = Number(text);
|
|
1021
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function usageSpendFromObject(value) {
|
|
1025
|
+
if (!value) return 0;
|
|
1026
|
+
if (Array.isArray(value)) return value.reduce((sum, item) => sum + usageSpendFromObject(item), 0);
|
|
1027
|
+
if (typeof value !== "object") return 0;
|
|
1028
|
+
const directKeys = [
|
|
1029
|
+
"cost_usd",
|
|
1030
|
+
"total_cost_usd",
|
|
1031
|
+
"billed_usd",
|
|
1032
|
+
"spend_usd",
|
|
1033
|
+
"amount_usd",
|
|
1034
|
+
"total_usd",
|
|
1035
|
+
"cost",
|
|
1036
|
+
"total_cost",
|
|
1037
|
+
"amount",
|
|
1038
|
+
"usd"
|
|
1039
|
+
];
|
|
1040
|
+
for (const key of directKeys) {
|
|
1041
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
1042
|
+
const amount = moneyNumber(value[key]);
|
|
1043
|
+
if (amount > 0) return amount;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const nestedKeys = ["data", "rows", "items", "usage", "results", "records"];
|
|
1047
|
+
let total = 0;
|
|
1048
|
+
for (const key of nestedKeys) {
|
|
1049
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) total += usageSpendFromObject(value[key]);
|
|
1050
|
+
}
|
|
1051
|
+
return total;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function parseCsvSpend(text) {
|
|
1055
|
+
const lines = String(text || "").split(/\r?\n/).filter(Boolean);
|
|
1056
|
+
if (lines.length < 2) return 0;
|
|
1057
|
+
const headers = lines[0].split(",").map((item) => item.trim().replace(/^"|"$/g, "").toLowerCase());
|
|
1058
|
+
const spendColumns = headers
|
|
1059
|
+
.map((name, index) => ({ name, index }))
|
|
1060
|
+
.filter(({ name }) => /^(cost_usd|total_cost_usd|billed_usd|spend_usd|amount_usd|total_usd|cost|amount|usd)$/.test(name));
|
|
1061
|
+
if (!spendColumns.length) return 0;
|
|
1062
|
+
let total = 0;
|
|
1063
|
+
for (const line of lines.slice(1)) {
|
|
1064
|
+
const cells = line.split(",");
|
|
1065
|
+
const column = spendColumns.find(({ index }) => moneyNumber(cells[index]) > 0);
|
|
1066
|
+
if (column) total += moneyNumber(cells[column.index]);
|
|
1067
|
+
}
|
|
1068
|
+
return total;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function loadSpendInput(args) {
|
|
1072
|
+
const supplied = moneyNumber(args["monthly-spend"] || args.spend);
|
|
1073
|
+
if (supplied > 0) return { monthlySpendUsd: supplied, source: "user-supplied monthly spend" };
|
|
1074
|
+
const usageFile = args["usage-file"] ? path.resolve(String(args["usage-file"])) : "";
|
|
1075
|
+
if (!usageFile) return { monthlySpendUsd: 0, source: "not supplied" };
|
|
1076
|
+
if (!fs.existsSync(usageFile)) {
|
|
1077
|
+
process.stderr.write(`Usage file not found: ${usageFile}\n`);
|
|
1078
|
+
process.exit(2);
|
|
1079
|
+
}
|
|
1080
|
+
const text = fs.readFileSync(usageFile, "utf8");
|
|
1081
|
+
let total = 0;
|
|
1082
|
+
if (usageFile.toLowerCase().endsWith(".json")) {
|
|
1083
|
+
try {
|
|
1084
|
+
total = usageSpendFromObject(JSON.parse(text));
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
process.stderr.write(`Could not parse usage JSON: ${err.message}\n`);
|
|
1087
|
+
process.exit(2);
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
total = parseCsvSpend(text);
|
|
1091
|
+
}
|
|
1092
|
+
return { monthlySpendUsd: total, source: `usage import: ${usageFile}` };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function buildAuditMarkdown(audit) {
|
|
1096
|
+
return `# CostLayers AI Spend Audit
|
|
1097
|
+
|
|
1098
|
+
Generated: ${audit.created_utc}
|
|
1099
|
+
Repo: ${audit.repo}
|
|
1100
|
+
|
|
1101
|
+
## Executive Summary
|
|
1102
|
+
|
|
1103
|
+
- AI spend analyzed: ${audit.spend_analyzed_display}
|
|
1104
|
+
- Candidate waste found: ${audit.waste_found_display}
|
|
1105
|
+
- Quality-safe savings available now: ${audit.safe_savings_display}
|
|
1106
|
+
- First-scan context reduction: ${audit.context_reduction_percent}%
|
|
1107
|
+
- Repeated-context tokens avoided/run: ${audit.tokens_avoided_per_run.toLocaleString()}
|
|
1108
|
+
|
|
1109
|
+
## Evidence Labels
|
|
1110
|
+
|
|
1111
|
+
- Safe estimate: local repo scan found repeated context that agents can avoid before broad exploration.
|
|
1112
|
+
- Verified invoice savings: requires API-mode traffic through CostLayers and provider cost rows.
|
|
1113
|
+
- Risky savings: not counted here. CostLayers should forward unchanged when a reduction boundary is unsafe.
|
|
1114
|
+
|
|
1115
|
+
## Recommended Next Step
|
|
1116
|
+
|
|
1117
|
+
${audit.next_step}
|
|
1118
|
+
|
|
1119
|
+
## Largest Repeated Context Sources
|
|
1120
|
+
|
|
1121
|
+
${audit.largest_files.map((row) => `- ${row.path}: ${row.tokens.toLocaleString()} tokens`).join("\n")}
|
|
1122
|
+
`;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function buildAuditReport(repo, report, args) {
|
|
1126
|
+
const projection = savingsProjection(report);
|
|
1127
|
+
const spendInput = loadSpendInput(args);
|
|
1128
|
+
const spendAnalyzed = spendInput.monthlySpendUsd;
|
|
1129
|
+
const contextSafeSavings = projection.monthlyUsd;
|
|
1130
|
+
const contextRatio = Math.max(0, Math.min(1, Number(report.estimated_reduction_percent || 0) / 100));
|
|
1131
|
+
const budgetWasteEstimate = spendAnalyzed > 0 ? spendAnalyzed * Math.min(0.25, contextRatio * 0.2) : 0;
|
|
1132
|
+
const wasteFound = Math.max(contextSafeSavings, budgetWasteEstimate);
|
|
1133
|
+
const safeSavings = contextSafeSavings;
|
|
1134
|
+
return {
|
|
1135
|
+
created_utc: new Date().toISOString(),
|
|
1136
|
+
repo: path.basename(repo),
|
|
1137
|
+
spend_source: spendInput.source,
|
|
1138
|
+
monthly_spend_analyzed_usd: Number(spendAnalyzed.toFixed(6)),
|
|
1139
|
+
waste_found_usd_per_month: Number(wasteFound.toFixed(6)),
|
|
1140
|
+
quality_safe_savings_usd_per_month: Number(safeSavings.toFixed(6)),
|
|
1141
|
+
context_reduction_percent: Number(report.estimated_reduction_percent || 0),
|
|
1142
|
+
tokens_avoided_per_run: Number(report.tokens_avoided_per_repeated_task || 0),
|
|
1143
|
+
source_tokens_indexed: Number(report.source_tokens_indexed || 0),
|
|
1144
|
+
context_pack_tokens: Number(report.context_pack_tokens || 0),
|
|
1145
|
+
largest_files: report.largest_files || [],
|
|
1146
|
+
spend_analyzed_display: spendAnalyzed > 0 ? `${formatUsd(spendAnalyzed)}/month (${spendInput.source})` : "not supplied; using repo-scan savings only",
|
|
1147
|
+
waste_found_display: `${formatUsd(wasteFound)}/month candidate estimate`,
|
|
1148
|
+
safe_savings_display: `${formatUsd(safeSavings)}/month from local context scan`,
|
|
1149
|
+
next_step: spendAnalyzed > 0
|
|
1150
|
+
? "Enable API invoice mode on one controlled coding-agent workflow so CostLayers can turn the audit estimate into verified provider-dollar savings."
|
|
1151
|
+
: "Rerun with --monthly-spend <usd> or --usage-file <path>, then enable API invoice mode on one controlled workflow."
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function writeAuditReport(outDir, repo, report, args) {
|
|
1156
|
+
const auditReport = buildAuditReport(repo, report, args);
|
|
1157
|
+
fs.writeFileSync(path.join(outDir, "ai-spend-audit.json"), JSON.stringify(auditReport, null, 2) + "\n", "utf8");
|
|
1158
|
+
fs.writeFileSync(path.join(outDir, "ai-spend-audit.md"), buildAuditMarkdown(auditReport), "utf8");
|
|
1159
|
+
return auditReport;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
async function audit(repo, args) {
|
|
1163
|
+
process.stdout.write(`Running CostLayers AI spend audit for: ${repo}\n`);
|
|
1164
|
+
const precomputed = scanToFiles(repo, args);
|
|
1165
|
+
const { outDir, report } = precomputed;
|
|
1166
|
+
const auditReport = writeAuditReport(outDir, repo, report, args);
|
|
1167
|
+
process.stdout.write(`\nCostLayers AI Spend Audit\n`);
|
|
1168
|
+
process.stdout.write(`\nYou found repeated context waste\n`);
|
|
1169
|
+
process.stdout.write(` AI spend analyzed: ${auditReport.spend_analyzed_display}\n`);
|
|
1170
|
+
process.stdout.write(` Candidate waste found: ${auditReport.waste_found_display}\n`);
|
|
1171
|
+
process.stdout.write(` Quality-safe savings available now: ${auditReport.safe_savings_display}\n`);
|
|
1172
|
+
process.stdout.write(` Context reduction: ${auditReport.context_reduction_percent}%\n`);
|
|
1173
|
+
process.stdout.write(` Tokens avoided per agent run: ${formatInt(auditReport.tokens_avoided_per_run)}\n`);
|
|
1174
|
+
process.stdout.write(` Audit report: ${path.join(outDir, "ai-spend-audit.md")}\n`);
|
|
1175
|
+
process.stdout.write(`\nView this result any time:\n`);
|
|
1176
|
+
process.stdout.write(` npx -y ${INSTALL_SPEC} dashboard\n`);
|
|
1177
|
+
process.stdout.write(`\nNext best step:\n`);
|
|
1178
|
+
process.stdout.write(` ${auditReport.next_step}\n`);
|
|
1179
|
+
process.stdout.write(`\nHosted dashboard, optional:\n`);
|
|
1180
|
+
process.stdout.write(` npx -y ${INSTALL_SPEC} signup --email you@example.com\n`);
|
|
1181
|
+
await trackEvent(repo, args, "ai_spend_audit", {
|
|
1182
|
+
monthly_spend_analyzed_usd: auditReport.monthly_spend_analyzed_usd,
|
|
1183
|
+
waste_found_usd_per_month: auditReport.waste_found_usd_per_month,
|
|
1184
|
+
quality_safe_savings_usd_per_month: auditReport.quality_safe_savings_usd_per_month,
|
|
1185
|
+
context_tokens_avoided_per_task: auditReport.tokens_avoided_per_run
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
954
1189
|
function codexHomeDir() {
|
|
955
1190
|
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
956
1191
|
}
|
|
@@ -1154,7 +1389,10 @@ async function signup(repo, args) {
|
|
|
1154
1389
|
process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
1155
1390
|
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
1156
1391
|
process.stdout.write(`Plan: free beta\n`);
|
|
1157
|
-
process.stdout.write(
|
|
1392
|
+
process.stdout.write(`\nNext:\n`);
|
|
1393
|
+
process.stdout.write(` View dashboard: npx -y ${INSTALL_SPEC} dashboard\n`);
|
|
1394
|
+
process.stdout.write(` Run Codex with usage-stretch metering: npx -y ${INSTALL_SPEC} codex --email ${connection.email || "you@example.com"} --chatgpt\n`);
|
|
1395
|
+
process.stdout.write(` Prove API invoice savings: export OPENAI_API_KEY=sk-proj-... && npx -y ${INSTALL_SPEC} test --email ${connection.email || "you@example.com"}\n`);
|
|
1158
1396
|
}
|
|
1159
1397
|
|
|
1160
1398
|
async function codexProfile(repo, args) {
|
|
@@ -1327,24 +1565,28 @@ async function runAgent(repo, args, argv, options = {}) {
|
|
|
1327
1565
|
if (connection && connection.engine_url) {
|
|
1328
1566
|
try {
|
|
1329
1567
|
plan = await fetchEnginePlan(connection, repo, pack, report);
|
|
1330
|
-
process.stdout.write(`Fetched CostLayers engine plan\n`);
|
|
1568
|
+
if (!options.compactOutput) process.stdout.write(`Fetched CostLayers engine plan\n`);
|
|
1331
1569
|
} catch (err) {
|
|
1332
1570
|
process.stderr.write(`Engine unavailable; falling back to local plan: ${err.message}\n`);
|
|
1333
1571
|
}
|
|
1334
1572
|
}
|
|
1335
1573
|
writeRuntimePrompt(outDir, plan);
|
|
1336
|
-
process.stdout.write(`Runtime plan: ${path.join(outDir, "runtime-plan.md")}\n`);
|
|
1574
|
+
if (!options.compactOutput) process.stdout.write(`Runtime plan: ${path.join(outDir, "runtime-plan.md")}\n`);
|
|
1337
1575
|
let commandToRun = command;
|
|
1338
1576
|
if (connection && connection.engine_url && isCodexCommand(command)) {
|
|
1339
1577
|
assertCodexProxyApiKey(args);
|
|
1340
1578
|
const profilePath = writeCodexProfile(connection, args);
|
|
1341
1579
|
commandToRun = withCostLayersCodexProfile(command);
|
|
1342
1580
|
await trackEvent(repo, args, "codex_run", { profile_mode: codexProxyEnabled(args) ? "api" : "chatgpt" }, connection);
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1581
|
+
if (options.compactOutput) {
|
|
1582
|
+
process.stdout.write(`Launching Codex with CostLayers profile: ${commandToRun.join(" ")}\n`);
|
|
1583
|
+
} else {
|
|
1584
|
+
process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
|
|
1585
|
+
process.stdout.write(`Codex metering enabled: ${commandToRun.join(" ")}\n`);
|
|
1586
|
+
process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
|
|
1587
|
+
process.stdout.write(`Savings dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
1588
|
+
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
1589
|
+
}
|
|
1348
1590
|
}
|
|
1349
1591
|
const env = {
|
|
1350
1592
|
...process.env,
|
|
@@ -1417,8 +1659,19 @@ async function gateway(repo, args) {
|
|
|
1417
1659
|
}
|
|
1418
1660
|
|
|
1419
1661
|
async function dashboard(repo, args) {
|
|
1420
|
-
const connection =
|
|
1421
|
-
|
|
1662
|
+
const connection = loadStoredConnection(repo);
|
|
1663
|
+
if (!connection || !connection.engine_url || !connection.api_key) {
|
|
1664
|
+
if (printLocalDashboard(repo)) return;
|
|
1665
|
+
process.exit(2);
|
|
1666
|
+
}
|
|
1667
|
+
let status;
|
|
1668
|
+
try {
|
|
1669
|
+
status = await postJson(`${connection.engine_url}/v1/me`, {}, connection.api_key);
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
process.stderr.write(`Hosted dashboard unavailable: ${err.message}\n\n`);
|
|
1672
|
+
if (printLocalDashboard(repo)) return;
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
}
|
|
1422
1675
|
await trackEvent(repo, args, "dashboard_open", {}, connection);
|
|
1423
1676
|
const dashboardUrl = (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
|
|
1424
1677
|
process.stdout.write(`CostLayers Dashboard\n`);
|
|
@@ -1440,11 +1693,11 @@ async function codexShortcut(repo, args, argv) {
|
|
|
1440
1693
|
const codexTail = codexArgsAfterDash(argv);
|
|
1441
1694
|
const command = codexTail.length > 0 ? codexTail : ["codex"];
|
|
1442
1695
|
const commandToRun = isCodexCommand(command) ? command : ["codex", ...command];
|
|
1443
|
-
const nextArgs = withAutoCodexMode(args);
|
|
1696
|
+
const nextArgs = withAutoCodexMode({ ...args, "ux-compact": true });
|
|
1444
1697
|
if (codexProxyEnabled(nextArgs)) {
|
|
1445
1698
|
process.stdout.write(`CostLayers Codex: API invoice mode explicitly enabled from ${codexProxyApiKeyEnv(nextArgs)}.\n`);
|
|
1446
1699
|
} else {
|
|
1447
|
-
process.stdout.write(`CostLayers Codex: ChatGPT usage-stretch mode.
|
|
1700
|
+
process.stdout.write(`CostLayers Codex: ChatGPT usage-stretch mode. Native Codex auth stays unchanged.\n`);
|
|
1448
1701
|
}
|
|
1449
1702
|
return start(repo, nextArgs, ["start", "--", ...commandToRun]);
|
|
1450
1703
|
}
|
|
@@ -1485,16 +1738,18 @@ async function savingsTest(repo, args) {
|
|
|
1485
1738
|
async function start(repo, args, argv, options = {}) {
|
|
1486
1739
|
const dash = argv.indexOf("--");
|
|
1487
1740
|
const command = dash >= 0 ? argv.slice(dash + 1) : [];
|
|
1741
|
+
const compactOutput = Boolean(args["ux-compact"]);
|
|
1488
1742
|
const codexTelemetryRun = command.length > 0 && isCodexCommand(command) && !codexProxyEnabled(args);
|
|
1489
1743
|
if (command.length > 0 && isCodexCommand(command)) assertCodexProxyApiKey(args);
|
|
1490
1744
|
init(repo, { suppressNext: true });
|
|
1491
1745
|
process.stdout.write(`Scanning repo: ${repo}\n`);
|
|
1492
1746
|
const precomputed = scanToFiles(repo, args);
|
|
1493
1747
|
const { outDir, pack, report } = precomputed;
|
|
1748
|
+
writeAuditReport(outDir, repo, report, args);
|
|
1494
1749
|
process.stdout.write(`CostLayers scan complete\n`);
|
|
1495
1750
|
if (precomputed.localCacheHit) process.stdout.write(`Local exact cache hit: source hashes unchanged, reused CostLayers artifacts\n`);
|
|
1496
|
-
process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
|
|
1497
|
-
printSavingsSummary(report);
|
|
1751
|
+
if (!compactOutput) process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
|
|
1752
|
+
printSavingsSummary(report, { compact: compactOutput });
|
|
1498
1753
|
const connection = await ensureConnection(repo, args);
|
|
1499
1754
|
await trackEvent(repo, args, "cli_start", {
|
|
1500
1755
|
files_indexed: report.files_indexed,
|
|
@@ -1507,11 +1762,15 @@ async function start(repo, args, argv, options = {}) {
|
|
|
1507
1762
|
} catch (err) {
|
|
1508
1763
|
process.stderr.write(`Dashboard sync delayed; local report is still available: ${err.message}\n`);
|
|
1509
1764
|
}
|
|
1510
|
-
process.stdout.write(`CostLayers connection ready\n`);
|
|
1765
|
+
if (!compactOutput) process.stdout.write(`CostLayers connection ready\n`);
|
|
1511
1766
|
let gatewayBaseUrl = connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key);
|
|
1512
1767
|
if (codexTelemetryRun) {
|
|
1513
|
-
|
|
1514
|
-
|
|
1768
|
+
if (compactOutput) {
|
|
1769
|
+
process.stdout.write(`Mode: ChatGPT-login Codex. Model calls are not routed; CostLayers adds repo context discipline and usage-stretch metering.\n`);
|
|
1770
|
+
} else {
|
|
1771
|
+
process.stdout.write(`ChatGPT-login Codex mode: native Codex provider preserved; model calls are not routed through CostLayers.\n`);
|
|
1772
|
+
process.stdout.write(`What users get: repo context discipline and a usage-stretch meter. This does not reduce a flat ChatGPT subscription invoice.\n`);
|
|
1773
|
+
}
|
|
1515
1774
|
} else {
|
|
1516
1775
|
const providerUrl = typeof args["provider-url"] === "string" ? args["provider-url"] : "https://api.openai.com";
|
|
1517
1776
|
const payload = {
|
|
@@ -1537,13 +1796,15 @@ async function start(repo, args, argv, options = {}) {
|
|
|
1537
1796
|
}
|
|
1538
1797
|
}
|
|
1539
1798
|
process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
1540
|
-
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
1799
|
+
if (!compactOutput) process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
1541
1800
|
process.stdout.write(`Plan: free beta\n`);
|
|
1542
1801
|
const profilePath = writeCodexProfile(connection, args);
|
|
1543
|
-
|
|
1544
|
-
|
|
1802
|
+
if (!compactOutput) {
|
|
1803
|
+
process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
|
|
1804
|
+
process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
|
|
1805
|
+
}
|
|
1545
1806
|
if (command.length > 0) {
|
|
1546
|
-
return runAgent(repo, args, argv, { skipSetup: true, precomputed, returnStatus: options.returnStatus });
|
|
1807
|
+
return runAgent(repo, args, argv, { skipSetup: true, precomputed, returnStatus: options.returnStatus, compactOutput });
|
|
1547
1808
|
}
|
|
1548
1809
|
process.stdout.write(`\nReady.\n`);
|
|
1549
1810
|
process.stdout.write(` Run Codex: codex --profile costlayers\n`);
|
|
@@ -1581,8 +1842,9 @@ function main() {
|
|
|
1581
1842
|
const cmd = args._[0];
|
|
1582
1843
|
if (!cmd || args.help || args.h) usage(0);
|
|
1583
1844
|
const repo = path.resolve(String(args.repo || process.cwd()));
|
|
1584
|
-
if (["init", "scan", "start", "run", "codex-profile", "codex", "test"].includes(cmd)) guardRepoRoot(repo, args);
|
|
1845
|
+
if (["init", "scan", "audit", "start", "run", "codex-profile", "codex", "test"].includes(cmd)) guardRepoRoot(repo, args);
|
|
1585
1846
|
if (cmd === "doctor") return doctor();
|
|
1847
|
+
if (cmd === "audit") return audit(repo, args);
|
|
1586
1848
|
if (cmd === "codex") return codexShortcut(repo, args, rawArgv);
|
|
1587
1849
|
if (cmd === "test") return savingsTest(repo, args);
|
|
1588
1850
|
if (cmd === "init") return init(repo);
|
package/package.json
CHANGED