costlayers 0.8.14 → 0.8.16
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 +15 -13
- package/bin/agentspend.js +121 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,29 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
CostLayers helps coding-agent users stop paying for repeated repo context. API users can route model calls through the gateway for invoice savings. ChatGPT-login Codex users get a usage-stretch meter that shows how much repeated context was avoided.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Daily Codex use:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
cd your-repo
|
|
11
|
-
npx -y costlayers@latest
|
|
11
|
+
npx -y costlayers@latest codex --email you@example.com
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
If `OPENAI_API_KEY` is set, CostLayers automatically uses API invoice mode.
|
|
15
|
+
If no API key is set, it uses ChatGPT-login usage-stretch mode.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Run a one-command API savings test:
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
|
-
costlayers
|
|
20
|
-
costlayers scan
|
|
20
|
+
npx -y costlayers@latest test --email you@example.com
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Inside a repo:
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
|
-
|
|
27
|
-
npx -y costlayers@latest start --email you@example.com -- codex
|
|
28
|
+
npx -y costlayers@latest codex --email you@example.com
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
This gives Codex `.agentspend/repo-pack.md` and `.agentspend/runtime-plan.md`
|
|
@@ -44,7 +45,7 @@ API write permission:
|
|
|
44
45
|
|
|
45
46
|
```bash
|
|
46
47
|
export OPENAI_API_KEY=sk-proj-...
|
|
47
|
-
npx -y costlayers@latest
|
|
48
|
+
npx -y costlayers@latest codex --email you@example.com
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
ChatGPT-login Codex can be metered, but it does not create per-request OpenAI
|
|
@@ -52,8 +53,9 @@ Platform invoice savings because it is not billed through your Platform API key.
|
|
|
52
53
|
|
|
53
54
|
## Which Mode Should I Use?
|
|
54
55
|
|
|
55
|
-
- ChatGPT-login Codex: use `
|
|
56
|
-
- OpenAI Platform API billing: use
|
|
56
|
+
- ChatGPT-login Codex: use `costlayers codex --email you@example.com --chatgpt` to reduce repeated repo context and stretch usage limits.
|
|
57
|
+
- OpenAI Platform API billing: set `OPENAI_API_KEY`, then use `costlayers codex --email you@example.com` for invoice-backed savings.
|
|
58
|
+
- Savings proof: set `OPENAI_API_KEY`, then run `costlayers test --email you@example.com`.
|
|
57
59
|
- Other OpenAI-compatible clients: point the client at the CostLayers gateway URL and check `costlayers gateway report`.
|
|
58
60
|
|
|
59
61
|
To install only the Codex profile after signup:
|
package/bin/agentspend.js
CHANGED
|
@@ -9,7 +9,7 @@ const https = require("https");
|
|
|
9
9
|
const os = require("os");
|
|
10
10
|
const { spawnSync } = require("child_process");
|
|
11
11
|
|
|
12
|
-
const VERSION = "0.8.
|
|
12
|
+
const VERSION = "0.8.16";
|
|
13
13
|
const INSTALL_SPEC = "costlayers@latest";
|
|
14
14
|
const DEFAULT_RUNS_PER_WEEK = 20;
|
|
15
15
|
const WEEKS_PER_MONTH = 4.33;
|
|
@@ -50,6 +50,8 @@ function usage(exitCode = 0) {
|
|
|
50
50
|
CostLayers ${VERSION}
|
|
51
51
|
|
|
52
52
|
Usage:
|
|
53
|
+
costlayers codex [--email <email>] [--chatgpt|--api] [-- <codex args>]
|
|
54
|
+
costlayers test [--email <email>] [--prompt <text>]
|
|
53
55
|
costlayers init [--repo <path>]
|
|
54
56
|
costlayers scan [--repo <path>] [--price-per-1m <usd>] [--tasks <n>] [--runs-per-week <n>]
|
|
55
57
|
costlayers start [--email <email>] [--provider-url <url>] [--mode measure|reduce] [--runs-per-week <n>] [--codex-proxy] [-- <agent command>]
|
|
@@ -64,6 +66,8 @@ Usage:
|
|
|
64
66
|
costlayers doctor
|
|
65
67
|
|
|
66
68
|
Commands:
|
|
69
|
+
codex Start Codex with CostLayers. Uses API invoice mode automatically when OPENAI_API_KEY is set.
|
|
70
|
+
test Run a safe read-only Codex task and print the CostLayers savings report.
|
|
67
71
|
init Create .agentspend config and agent instructions.
|
|
68
72
|
scan Build repo context pack and savings report.
|
|
69
73
|
start One-command setup, signup, gateway start, and optional agent run.
|
|
@@ -123,7 +127,7 @@ function guardRepoRoot(repo, args) {
|
|
|
123
127
|
"",
|
|
124
128
|
"Run it inside a project folder instead:",
|
|
125
129
|
" cd path/to/your-repo",
|
|
126
|
-
` npx -y ${INSTALL_SPEC}
|
|
130
|
+
` npx -y ${INSTALL_SPEC} codex --email you@example.com`,
|
|
127
131
|
"",
|
|
128
132
|
"Or pass --repo path/to/your-repo from anywhere.",
|
|
129
133
|
"If you really intend to scan your whole home directory, add --allow-home.",
|
|
@@ -145,6 +149,10 @@ function readJsonIfExists(file) {
|
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
function normalizedEmail(value) {
|
|
153
|
+
return String(value || "").trim().toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
function estimateTokens(text) {
|
|
149
157
|
return Math.ceil(String(text || "").length / 4);
|
|
150
158
|
}
|
|
@@ -534,6 +542,28 @@ function codexProxyApiKeyEnv(args = {}) {
|
|
|
534
542
|
return String(args["api-key-env"] || "OPENAI_API_KEY");
|
|
535
543
|
}
|
|
536
544
|
|
|
545
|
+
function apiInvoiceModeRequested(args = {}) {
|
|
546
|
+
return Boolean(args.api || args.invoice || args["api-mode"] || codexProxyEnabled(args));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function chatgptModeRequested(args = {}) {
|
|
550
|
+
return Boolean(args.chatgpt || args["no-api"]);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function codexArgsAfterDash(argv) {
|
|
554
|
+
const dash = argv.indexOf("--");
|
|
555
|
+
return dash >= 0 ? argv.slice(dash + 1) : [];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function withAutoCodexMode(args = {}, options = {}) {
|
|
559
|
+
const next = { ...args };
|
|
560
|
+
const keyEnv = codexProxyApiKeyEnv(next);
|
|
561
|
+
const hasApiKey = Boolean(process.env[keyEnv]);
|
|
562
|
+
const wantsInvoice = apiInvoiceModeRequested(next) || (options.preferInvoice && hasApiKey);
|
|
563
|
+
if (!chatgptModeRequested(next) && wantsInvoice) next["codex-proxy"] = true;
|
|
564
|
+
return next;
|
|
565
|
+
}
|
|
566
|
+
|
|
537
567
|
function assertCodexProxyApiKey(args = {}) {
|
|
538
568
|
if (!codexProxyEnabled(args)) return;
|
|
539
569
|
const keyEnv = codexProxyApiKeyEnv(args);
|
|
@@ -543,7 +573,7 @@ function assertCodexProxyApiKey(args = {}) {
|
|
|
543
573
|
"",
|
|
544
574
|
"Set an OpenAI Platform API key with Responses API write permission, then rerun:",
|
|
545
575
|
` export ${keyEnv}=sk-proj-...`,
|
|
546
|
-
` npx -y ${INSTALL_SPEC}
|
|
576
|
+
` npx -y ${INSTALL_SPEC} codex --email you@example.com --api`,
|
|
547
577
|
"",
|
|
548
578
|
"ChatGPT-login Codex can be metered, but it cannot produce provider invoice savings because there is no per-request Platform invoice to reduce.",
|
|
549
579
|
""
|
|
@@ -639,16 +669,49 @@ async function signupConnection(repo, args) {
|
|
|
639
669
|
engine_url: result.engine_url || engineUrl,
|
|
640
670
|
api_key: result.api_key,
|
|
641
671
|
gateway_url: result.gateway_url,
|
|
672
|
+
email: normalizedEmail(payload.email),
|
|
673
|
+
label: payload.label,
|
|
642
674
|
connected_utc: new Date().toISOString()
|
|
643
675
|
};
|
|
644
676
|
fs.writeFileSync(path.join(outDir, "connection.json"), JSON.stringify(connection, null, 2) + "\n", "utf8");
|
|
645
677
|
return connection;
|
|
646
678
|
}
|
|
647
679
|
|
|
680
|
+
function saveConnection(repo, connection) {
|
|
681
|
+
const outDir = path.join(repo, ".agentspend");
|
|
682
|
+
ensureDir(outDir);
|
|
683
|
+
fs.writeFileSync(path.join(outDir, "connection.json"), JSON.stringify(connection, null, 2) + "\n", "utf8");
|
|
684
|
+
}
|
|
685
|
+
|
|
648
686
|
async function ensureConnection(repo, args) {
|
|
649
687
|
const outDir = path.join(repo, ".agentspend");
|
|
650
688
|
const saved = readJsonIfExists(path.join(outDir, "connection.json"));
|
|
651
|
-
|
|
689
|
+
const requestedEmail = normalizedEmail(args.email);
|
|
690
|
+
if (saved && saved.engine_url && saved.api_key) {
|
|
691
|
+
if (!requestedEmail) return saved;
|
|
692
|
+
const savedEmail = normalizedEmail(saved.email);
|
|
693
|
+
if (savedEmail === requestedEmail) return saved;
|
|
694
|
+
if (savedEmail && savedEmail !== requestedEmail) {
|
|
695
|
+
process.stdout.write(`CostLayers email changed from ${savedEmail} to ${requestedEmail}; creating a new repo key.\n`);
|
|
696
|
+
return signupConnection(repo, args);
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
const status = await postJson(`${String(saved.engine_url).replace(/\/+$/, "")}/v1/me`, {}, saved.api_key);
|
|
700
|
+
const remoteEmail = normalizedEmail(status.email);
|
|
701
|
+
if (remoteEmail && remoteEmail !== requestedEmail) {
|
|
702
|
+
process.stdout.write(`CostLayers saved key belongs to ${remoteEmail}; creating a new repo key for ${requestedEmail}.\n`);
|
|
703
|
+
return signupConnection(repo, args);
|
|
704
|
+
}
|
|
705
|
+
if (remoteEmail === requestedEmail) {
|
|
706
|
+
const updated = { ...saved, email: remoteEmail };
|
|
707
|
+
saveConnection(repo, updated);
|
|
708
|
+
return updated;
|
|
709
|
+
}
|
|
710
|
+
} catch (err) {
|
|
711
|
+
process.stdout.write(`CostLayers could not verify the saved key email (${err.message}); creating a new repo key for ${requestedEmail}.\n`);
|
|
712
|
+
return signupConnection(repo, args);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
652
715
|
return signupConnection(repo, args);
|
|
653
716
|
}
|
|
654
717
|
|
|
@@ -808,7 +871,9 @@ async function runAgent(repo, args, argv, options = {}) {
|
|
|
808
871
|
stdio: "inherit",
|
|
809
872
|
shell: process.platform === "win32"
|
|
810
873
|
});
|
|
811
|
-
|
|
874
|
+
const status = typeof result.status === "number" ? result.status : 1;
|
|
875
|
+
if (options.returnStatus) return status;
|
|
876
|
+
process.exit(status);
|
|
812
877
|
}
|
|
813
878
|
|
|
814
879
|
async function gateway(repo, args) {
|
|
@@ -881,7 +946,51 @@ async function dashboard(repo, args) {
|
|
|
881
946
|
process.stdout.write(`claim: API invoice savings only count when provider API traffic is routed through CostLayers. ChatGPT-login Codex shows usage stretch, not invoice reduction.\n`);
|
|
882
947
|
}
|
|
883
948
|
|
|
884
|
-
async function
|
|
949
|
+
async function codexShortcut(repo, args, argv) {
|
|
950
|
+
const codexTail = codexArgsAfterDash(argv);
|
|
951
|
+
const command = codexTail.length > 0 ? codexTail : ["codex"];
|
|
952
|
+
const commandToRun = isCodexCommand(command) ? command : ["codex", ...command];
|
|
953
|
+
const nextArgs = withAutoCodexMode(args, { preferInvoice: true });
|
|
954
|
+
if (codexProxyEnabled(nextArgs)) {
|
|
955
|
+
process.stdout.write(`CostLayers Codex: API invoice mode enabled from ${codexProxyApiKeyEnv(nextArgs)}.\n`);
|
|
956
|
+
} else {
|
|
957
|
+
process.stdout.write(`CostLayers Codex: ChatGPT usage-stretch mode. Set ${codexProxyApiKeyEnv(nextArgs)} or pass --api for invoice savings.\n`);
|
|
958
|
+
}
|
|
959
|
+
return start(repo, nextArgs, ["start", "--", ...commandToRun]);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async function savingsTest(repo, args) {
|
|
963
|
+
const nextArgs = withAutoCodexMode(args, { preferInvoice: true });
|
|
964
|
+
if (!codexProxyEnabled(nextArgs)) {
|
|
965
|
+
const keyEnv = codexProxyApiKeyEnv(nextArgs);
|
|
966
|
+
process.stderr.write([
|
|
967
|
+
`CostLayers test needs ${keyEnv} for real API invoice savings.`,
|
|
968
|
+
"",
|
|
969
|
+
"Set your OpenAI Platform API key, then rerun:",
|
|
970
|
+
` source ~/.config/costlayers/env`,
|
|
971
|
+
` npx -y ${INSTALL_SPEC} test --email you@example.com`,
|
|
972
|
+
"",
|
|
973
|
+
`For ChatGPT-login metering without invoice savings, run:`,
|
|
974
|
+
` npx -y ${INSTALL_SPEC} codex --email you@example.com --chatgpt`,
|
|
975
|
+
""
|
|
976
|
+
].join("\n"));
|
|
977
|
+
process.exit(2);
|
|
978
|
+
}
|
|
979
|
+
const prompt = typeof args.prompt === "string"
|
|
980
|
+
? args.prompt
|
|
981
|
+
: "Analyze this repository. Find the main entry points, data flow, and the 5 files most worth reading. Do not edit files.";
|
|
982
|
+
process.stdout.write("CostLayers savings test: running one safe read-only Codex task.\n");
|
|
983
|
+
const status = await start(repo, nextArgs, ["start", "--", "codex", "exec", "--sandbox", "read-only", prompt], { returnStatus: true });
|
|
984
|
+
process.stdout.write("\nCostLayers savings test report\n");
|
|
985
|
+
try {
|
|
986
|
+
await gateway(repo, { ...nextArgs, _: ["gateway", "report"] });
|
|
987
|
+
} catch (err) {
|
|
988
|
+
process.stderr.write(`Could not fetch savings report: ${err.message}\n`);
|
|
989
|
+
}
|
|
990
|
+
process.exit(status);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function start(repo, args, argv, options = {}) {
|
|
885
994
|
const dash = argv.indexOf("--");
|
|
886
995
|
const command = dash >= 0 ? argv.slice(dash + 1) : [];
|
|
887
996
|
const codexTelemetryRun = command.length > 0 && isCodexCommand(command) && !codexProxyEnabled(args);
|
|
@@ -935,12 +1044,12 @@ async function start(repo, args, argv) {
|
|
|
935
1044
|
process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
|
|
936
1045
|
process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
|
|
937
1046
|
if (command.length > 0) {
|
|
938
|
-
return runAgent(repo, args, argv, { skipSetup: true, precomputed });
|
|
1047
|
+
return runAgent(repo, args, argv, { skipSetup: true, precomputed, returnStatus: options.returnStatus });
|
|
939
1048
|
}
|
|
940
1049
|
process.stdout.write(`\nNext options:\n`);
|
|
941
1050
|
process.stdout.write(` Use gateway URL in your model client: ${gatewayBaseUrl}\n`);
|
|
942
|
-
process.stdout.write(` Run Codex
|
|
943
|
-
process.stdout.write(`
|
|
1051
|
+
process.stdout.write(` Run Codex: npx -y ${INSTALL_SPEC} codex --email you@example.com\n`);
|
|
1052
|
+
process.stdout.write(` Prove API savings: npx -y ${INSTALL_SPEC} test --email you@example.com\n`);
|
|
944
1053
|
process.stdout.write(` Or run Codex directly: codex --profile costlayers\n`);
|
|
945
1054
|
process.stdout.write(` View report: npx -y ${INSTALL_SPEC} gateway report\n`);
|
|
946
1055
|
process.stdout.write(` Dashboard: npx -y ${INSTALL_SPEC} dashboard\n`);
|
|
@@ -958,8 +1067,10 @@ function main() {
|
|
|
958
1067
|
const cmd = args._[0];
|
|
959
1068
|
if (!cmd || args.help || args.h) usage(0);
|
|
960
1069
|
const repo = path.resolve(String(args.repo || process.cwd()));
|
|
961
|
-
if (["init", "scan", "start", "run", "codex-profile"].includes(cmd)) guardRepoRoot(repo, args);
|
|
1070
|
+
if (["init", "scan", "start", "run", "codex-profile", "codex", "test"].includes(cmd)) guardRepoRoot(repo, args);
|
|
962
1071
|
if (cmd === "doctor") return doctor();
|
|
1072
|
+
if (cmd === "codex") return codexShortcut(repo, args, rawArgv);
|
|
1073
|
+
if (cmd === "test") return savingsTest(repo, args);
|
|
963
1074
|
if (cmd === "init") return init(repo);
|
|
964
1075
|
if (cmd === "scan") return scan(repo, args);
|
|
965
1076
|
if (cmd === "start") return start(repo, args, rawArgv);
|
package/package.json
CHANGED