costlayers 0.8.16 → 0.8.17

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 CHANGED
@@ -8,15 +8,15 @@ Daily Codex use:
8
8
 
9
9
  ```bash
10
10
  cd your-repo
11
- npx -y costlayers@latest codex --email you@example.com
11
+ npx -y costlayers@latest codex --email you@example.com --chatgpt
12
12
  ```
13
13
 
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.
14
+ This default path preserves Codex's native ChatGPT-login/provider flow and shows a usage-stretch meter. CostLayers only routes billable API traffic when you explicitly pass `--api`.
16
15
 
17
16
  Run a one-command API savings test:
18
17
 
19
18
  ```bash
19
+ export OPENAI_API_KEY=sk-proj-...
20
20
  npx -y costlayers@latest test --email you@example.com
21
21
  ```
22
22
 
@@ -25,7 +25,7 @@ npx -y costlayers@latest test --email you@example.com
25
25
  Inside a repo:
26
26
 
27
27
  ```bash
28
- npx -y costlayers@latest codex --email you@example.com
28
+ npx -y costlayers@latest codex --email you@example.com --chatgpt
29
29
  ```
30
30
 
31
31
  This gives Codex `.agentspend/repo-pack.md` and `.agentspend/runtime-plan.md`
@@ -45,7 +45,7 @@ API write permission:
45
45
 
46
46
  ```bash
47
47
  export OPENAI_API_KEY=sk-proj-...
48
- npx -y costlayers@latest codex --email you@example.com
48
+ npx -y costlayers@latest codex --email you@example.com --api
49
49
  ```
50
50
 
51
51
  ChatGPT-login Codex can be metered, but it does not create per-request OpenAI
@@ -54,7 +54,7 @@ Platform invoice savings because it is not billed through your Platform API key.
54
54
  ## Which Mode Should I Use?
55
55
 
56
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.
57
+ - OpenAI Platform API billing: set `OPENAI_API_KEY`, then use `costlayers codex --email you@example.com --api` for invoice-backed savings.
58
58
  - Savings proof: set `OPENAI_API_KEY`, then run `costlayers test --email you@example.com`.
59
59
  - Other OpenAI-compatible clients: point the client at the CostLayers gateway URL and check `costlayers gateway report`.
60
60
 
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.16";
12
+ const VERSION = "0.8.17";
13
13
  const INSTALL_SPEC = "costlayers@latest";
14
14
  const DEFAULT_RUNS_PER_WEEK = 20;
15
15
  const WEEKS_PER_MONTH = 4.33;
@@ -66,8 +66,8 @@ Usage:
66
66
  costlayers doctor
67
67
 
68
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.
69
+ codex Start Codex with CostLayers. Defaults to ChatGPT-login mode unless --api is passed.
70
+ test Run a safe read-only API invoice-mode Codex task and print the CostLayers savings report.
71
71
  init Create .agentspend config and agent instructions.
72
72
  scan Build repo context pack and savings report.
73
73
  start One-command setup, signup, gateway start, and optional agent run.
@@ -107,6 +107,14 @@ function ensureDir(dir) {
107
107
  fs.mkdirSync(dir, { recursive: true });
108
108
  }
109
109
 
110
+ function chmodBestEffort(file, mode) {
111
+ try {
112
+ fs.chmodSync(file, mode);
113
+ } catch {
114
+ // Windows and some network filesystems may ignore POSIX modes.
115
+ }
116
+ }
117
+
110
118
  function writeIfMissing(file, content) {
111
119
  if (!fs.existsSync(file)) fs.writeFileSync(file, content, "utf8");
112
120
  }
@@ -127,7 +135,7 @@ function guardRepoRoot(repo, args) {
127
135
  "",
128
136
  "Run it inside a project folder instead:",
129
137
  " cd path/to/your-repo",
130
- ` npx -y ${INSTALL_SPEC} codex --email you@example.com`,
138
+ ` npx -y ${INSTALL_SPEC} codex --email you@example.com --chatgpt`,
131
139
  "",
132
140
  "Or pass --repo path/to/your-repo from anywhere.",
133
141
  "If you really intend to scan your whole home directory, add --allow-home.",
@@ -153,6 +161,101 @@ function normalizedEmail(value) {
153
161
  return String(value || "").trim().toLowerCase();
154
162
  }
155
163
 
164
+ function configRoot() {
165
+ const base = process.env.XDG_CONFIG_HOME
166
+ ? path.join(process.env.XDG_CONFIG_HOME, "costlayers")
167
+ : path.join(os.homedir(), ".config", "costlayers");
168
+ ensureDir(base);
169
+ chmodBestEffort(base, 0o700);
170
+ return base;
171
+ }
172
+
173
+ function connectionsDir() {
174
+ const dir = path.join(configRoot(), "connections");
175
+ ensureDir(dir);
176
+ chmodBestEffort(dir, 0o700);
177
+ return dir;
178
+ }
179
+
180
+ function connectionSecretPath(connectionId) {
181
+ return path.join(connectionsDir(), `${connectionId}.json`);
182
+ }
183
+
184
+ function repoConnectionPath(repo) {
185
+ return path.join(repo, ".agentspend", "connection.json");
186
+ }
187
+
188
+ function ensureAgentSpendGitignore(outDir) {
189
+ ensureDir(outDir);
190
+ const file = path.join(outDir, ".gitignore");
191
+ const required = ["connection.json", "*.secret.json", "gateway-key.txt"];
192
+ let current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
193
+ const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
194
+ let changed = false;
195
+ for (const item of required) {
196
+ if (!lines.has(item)) {
197
+ current += `${current.endsWith("\n") || current.length === 0 ? "" : "\n"}${item}\n`;
198
+ changed = true;
199
+ }
200
+ }
201
+ if (changed || !fs.existsSync(file)) fs.writeFileSync(file, current, "utf8");
202
+ }
203
+
204
+ function connectionIdFor(repo, connection = {}) {
205
+ if (connection.connection_id) return String(connection.connection_id);
206
+ const seed = connection.api_key || `${path.resolve(repo)}\n${connection.engine_url || ""}\n${connection.email || ""}`;
207
+ return `cl_${sha256(seed).slice(0, 24)}`;
208
+ }
209
+
210
+ function publicConnectionMetadata(connection) {
211
+ return {
212
+ version: VERSION,
213
+ connection_id: connection.connection_id,
214
+ engine_url: connection.engine_url,
215
+ email: normalizedEmail(connection.email),
216
+ label: connection.label || "",
217
+ connected_utc: connection.connected_utc || new Date().toISOString(),
218
+ secret_store: "~/.config/costlayers/connections",
219
+ note: "Live CostLayers keys are stored outside the repo. Run `costlayers dashboard` to print your private dashboard URL."
220
+ };
221
+ }
222
+
223
+ function writePrivateJson(file, payload) {
224
+ ensureDir(path.dirname(file));
225
+ fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
226
+ chmodBestEffort(file, 0o600);
227
+ }
228
+
229
+ function saveConnection(repo, connection) {
230
+ const outDir = path.join(repo, ".agentspend");
231
+ ensureDir(outDir);
232
+ ensureAgentSpendGitignore(outDir);
233
+ const connectionId = connectionIdFor(repo, connection);
234
+ const full = {
235
+ ...connection,
236
+ connection_id: connectionId,
237
+ email: normalizedEmail(connection.email),
238
+ connected_utc: connection.connected_utc || new Date().toISOString()
239
+ };
240
+ writePrivateJson(connectionSecretPath(connectionId), full);
241
+ fs.writeFileSync(repoConnectionPath(repo), JSON.stringify(publicConnectionMetadata(full), null, 2) + "\n", "utf8");
242
+ return full;
243
+ }
244
+
245
+ function loadStoredConnection(repo) {
246
+ const saved = readJsonIfExists(repoConnectionPath(repo));
247
+ if (!saved) return null;
248
+ if (saved.api_key || saved.gateway_url) {
249
+ const migrated = saveConnection(repo, saved);
250
+ process.stdout.write(`CostLayers moved the live key out of .agentspend into ~/.config/costlayers.\n`);
251
+ return migrated;
252
+ }
253
+ const connectionId = saved.connection_id || connectionIdFor(repo, saved);
254
+ const secret = readJsonIfExists(connectionSecretPath(connectionId));
255
+ if (secret && secret.api_key) return { ...saved, ...secret, connection_id: connectionId };
256
+ return null;
257
+ }
258
+
156
259
  function estimateTokens(text) {
157
260
  return Math.ceil(String(text || "").length / 4);
158
261
  }
@@ -407,9 +510,10 @@ This public scanner estimates repeated context waste. Real savings should be val
407
510
  `;
408
511
  }
409
512
 
410
- function init(repo) {
513
+ function init(repo, options = {}) {
411
514
  const outDir = path.join(repo, ".agentspend");
412
515
  ensureDir(outDir);
516
+ ensureAgentSpendGitignore(outDir);
413
517
  writeIfMissing(path.join(outDir, "config.json"), JSON.stringify({
414
518
  version: VERSION,
415
519
  created_utc: new Date().toISOString(),
@@ -425,7 +529,7 @@ function init(repo) {
425
529
  process.stdout.write(`AGENTS.md already exists; snippet saved to ${path.join(outDir, "AGENTS_SNIPPET.md")}\n`);
426
530
  }
427
531
  process.stdout.write(`CostLayers initialized in ${outDir}\n`);
428
- process.stdout.write("Next: costlayers scan\n");
532
+ if (!options.suppressNext) process.stdout.write("Next: costlayers scan\n");
429
533
  }
430
534
 
431
535
  function scan(repo, args) {
@@ -451,21 +555,30 @@ function connectEngine(repo, args) {
451
555
  api_key: args["api-key"] ? String(args["api-key"]) : null,
452
556
  connected_utc: new Date().toISOString()
453
557
  };
454
- fs.writeFileSync(path.join(outDir, "connection.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
558
+ saveConnection(repo, config);
455
559
  process.stdout.write(`Connected CostLayers engine: ${config.engine_url}\n`);
456
560
  }
457
561
 
458
562
  function loadConnection(repo, args) {
459
- const outDir = path.join(repo, ".agentspend");
460
- const saved = readJsonIfExists(path.join(outDir, "connection.json")) || {};
563
+ const saved = loadStoredConnection(repo) || {};
461
564
  const engineUrl = String(args["engine-url"] || saved.engine_url || "").replace(/\/+$/, "");
462
565
  if (!engineUrl) {
463
- process.stderr.write("Missing engine connection. Run `agentspend connect --engine-url <url>` first.\n");
566
+ process.stderr.write("Missing engine connection. Run `costlayers signup --email you@example.com` first.\n");
567
+ process.exit(2);
568
+ }
569
+ const apiKey = args["api-key"] ? String(args["api-key"]) : saved.api_key || null;
570
+ if (!apiKey) {
571
+ process.stderr.write([
572
+ "CostLayers cannot find the live key for this repo.",
573
+ "Keys are stored outside the repo in ~/.config/costlayers/connections.",
574
+ "Run `costlayers signup --email you@example.com` again from this repo to create a fresh key.",
575
+ ""
576
+ ].join("\n"));
464
577
  process.exit(2);
465
578
  }
466
579
  return {
467
580
  engine_url: engineUrl,
468
- api_key: args["api-key"] ? String(args["api-key"]) : saved.api_key || null
581
+ api_key: apiKey
469
582
  };
470
583
  }
471
584
 
@@ -557,23 +670,22 @@ function codexArgsAfterDash(argv) {
557
670
 
558
671
  function withAutoCodexMode(args = {}, options = {}) {
559
672
  const next = { ...args };
560
- const keyEnv = codexProxyApiKeyEnv(next);
561
- const hasApiKey = Boolean(process.env[keyEnv]);
562
- const wantsInvoice = apiInvoiceModeRequested(next) || (options.preferInvoice && hasApiKey);
673
+ const wantsInvoice = apiInvoiceModeRequested(next) || Boolean(options.forceInvoice);
563
674
  if (!chatgptModeRequested(next) && wantsInvoice) next["codex-proxy"] = true;
564
675
  return next;
565
676
  }
566
677
 
567
- function assertCodexProxyApiKey(args = {}) {
678
+ function assertCodexProxyApiKey(args = {}, rerunCommand = "") {
568
679
  if (!codexProxyEnabled(args)) return;
569
680
  const keyEnv = codexProxyApiKeyEnv(args);
570
681
  if (process.env[keyEnv]) return;
682
+ const command = rerunCommand || `npx -y ${INSTALL_SPEC} codex --email you@example.com --api`;
571
683
  process.stderr.write([
572
684
  `CostLayers invoice mode needs ${keyEnv} in this shell.`,
573
685
  "",
574
686
  "Set an OpenAI Platform API key with Responses API write permission, then rerun:",
575
687
  ` export ${keyEnv}=sk-proj-...`,
576
- ` npx -y ${INSTALL_SPEC} codex --email you@example.com --api`,
688
+ ` ${command}`,
577
689
  "",
578
690
  "ChatGPT-login Codex can be metered, but it cannot produce provider invoice savings because there is no per-request Platform invoice to reduce.",
579
691
  ""
@@ -673,19 +785,11 @@ async function signupConnection(repo, args) {
673
785
  label: payload.label,
674
786
  connected_utc: new Date().toISOString()
675
787
  };
676
- fs.writeFileSync(path.join(outDir, "connection.json"), JSON.stringify(connection, null, 2) + "\n", "utf8");
677
- return connection;
678
- }
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");
788
+ return saveConnection(repo, connection);
684
789
  }
685
790
 
686
791
  async function ensureConnection(repo, args) {
687
- const outDir = path.join(repo, ".agentspend");
688
- const saved = readJsonIfExists(path.join(outDir, "connection.json"));
792
+ const saved = loadStoredConnection(repo);
689
793
  const requestedEmail = normalizedEmail(args.email);
690
794
  if (saved && saved.engine_url && saved.api_key) {
691
795
  if (!requestedEmail) return saved;
@@ -837,7 +941,7 @@ async function runAgent(repo, args, argv, options = {}) {
837
941
  }
838
942
  if (!options.skipSetup) init(repo);
839
943
  const { outDir, pack, report } = options.precomputed || scanToFiles(repo, args);
840
- const connection = readJsonIfExists(path.join(outDir, "connection.json"));
944
+ const connection = loadStoredConnection(repo);
841
945
  let plan = buildLocalPlan(report);
842
946
  if (connection && connection.engine_url) {
843
947
  try {
@@ -950,24 +1054,24 @@ async function codexShortcut(repo, args, argv) {
950
1054
  const codexTail = codexArgsAfterDash(argv);
951
1055
  const command = codexTail.length > 0 ? codexTail : ["codex"];
952
1056
  const commandToRun = isCodexCommand(command) ? command : ["codex", ...command];
953
- const nextArgs = withAutoCodexMode(args, { preferInvoice: true });
1057
+ const nextArgs = withAutoCodexMode(args);
954
1058
  if (codexProxyEnabled(nextArgs)) {
955
- process.stdout.write(`CostLayers Codex: API invoice mode enabled from ${codexProxyApiKeyEnv(nextArgs)}.\n`);
1059
+ process.stdout.write(`CostLayers Codex: API invoice mode explicitly enabled from ${codexProxyApiKeyEnv(nextArgs)}.\n`);
956
1060
  } else {
957
- process.stdout.write(`CostLayers Codex: ChatGPT usage-stretch mode. Set ${codexProxyApiKeyEnv(nextArgs)} or pass --api for invoice savings.\n`);
1061
+ process.stdout.write(`CostLayers Codex: ChatGPT usage-stretch mode. Pass --api to route API-billed provider calls for invoice savings.\n`);
958
1062
  }
959
1063
  return start(repo, nextArgs, ["start", "--", ...commandToRun]);
960
1064
  }
961
1065
 
962
1066
  async function savingsTest(repo, args) {
963
- const nextArgs = withAutoCodexMode(args, { preferInvoice: true });
1067
+ const nextArgs = withAutoCodexMode({ ...args, api: true }, { forceInvoice: true });
964
1068
  if (!codexProxyEnabled(nextArgs)) {
965
1069
  const keyEnv = codexProxyApiKeyEnv(nextArgs);
966
1070
  process.stderr.write([
967
1071
  `CostLayers test needs ${keyEnv} for real API invoice savings.`,
968
1072
  "",
969
1073
  "Set your OpenAI Platform API key, then rerun:",
970
- ` source ~/.config/costlayers/env`,
1074
+ ` export ${keyEnv}=sk-proj-...`,
971
1075
  ` npx -y ${INSTALL_SPEC} test --email you@example.com`,
972
1076
  "",
973
1077
  `For ChatGPT-login metering without invoice savings, run:`,
@@ -979,6 +1083,7 @@ async function savingsTest(repo, args) {
979
1083
  const prompt = typeof args.prompt === "string"
980
1084
  ? args.prompt
981
1085
  : "Analyze this repository. Find the main entry points, data flow, and the 5 files most worth reading. Do not edit files.";
1086
+ assertCodexProxyApiKey(nextArgs, `npx -y ${INSTALL_SPEC} test --email you@example.com`);
982
1087
  process.stdout.write("CostLayers savings test: running one safe read-only Codex task.\n");
983
1088
  const status = await start(repo, nextArgs, ["start", "--", "codex", "exec", "--sandbox", "read-only", prompt], { returnStatus: true });
984
1089
  process.stdout.write("\nCostLayers savings test report\n");
@@ -995,7 +1100,7 @@ async function start(repo, args, argv, options = {}) {
995
1100
  const command = dash >= 0 ? argv.slice(dash + 1) : [];
996
1101
  const codexTelemetryRun = command.length > 0 && isCodexCommand(command) && !codexProxyEnabled(args);
997
1102
  if (command.length > 0 && isCodexCommand(command)) assertCodexProxyApiKey(args);
998
- init(repo);
1103
+ init(repo, { suppressNext: true });
999
1104
  process.stdout.write(`Scanning repo: ${repo}\n`);
1000
1105
  const precomputed = scanToFiles(repo, args);
1001
1106
  const { outDir, pack, report } = precomputed;
@@ -1047,8 +1152,9 @@ async function start(repo, args, argv, options = {}) {
1047
1152
  return runAgent(repo, args, argv, { skipSetup: true, precomputed, returnStatus: options.returnStatus });
1048
1153
  }
1049
1154
  process.stdout.write(`\nNext options:\n`);
1050
- process.stdout.write(` Use gateway URL in your model client: ${gatewayBaseUrl}\n`);
1051
- process.stdout.write(` Run Codex: npx -y ${INSTALL_SPEC} codex --email you@example.com\n`);
1155
+ process.stdout.write(` ChatGPT-login Codex: npx -y ${INSTALL_SPEC} codex --email you@example.com --chatgpt\n`);
1156
+ process.stdout.write(` API invoice Codex: export OPENAI_API_KEY=sk-proj-... && npx -y ${INSTALL_SPEC} codex --email you@example.com --api\n`);
1157
+ process.stdout.write(` Other OpenAI-compatible client base URL: ${gatewayBaseUrl}\n`);
1052
1158
  process.stdout.write(` Prove API savings: npx -y ${INSTALL_SPEC} test --email you@example.com\n`);
1053
1159
  process.stdout.write(` Or run Codex directly: codex --profile costlayers\n`);
1054
1160
  process.stdout.write(` View report: npx -y ${INSTALL_SPEC} gateway report\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "costlayers",
3
- "version": "0.8.16",
3
+ "version": "0.8.17",
4
4
  "description": "CostLayers cost control for AI coding agents. Build compact repo context packs, gateway reports, and savings dashboards.",
5
5
  "bin": {
6
6
  "agentspend": "bin/agentspend.js",
@@ -8,6 +8,10 @@
8
8
  },
9
9
  "type": "commonjs",
10
10
  "license": "UNLICENSED",
11
+ "homepage": "https://costlayers.com",
12
+ "bugs": {
13
+ "url": "mailto:rishabh@costlayers.com"
14
+ },
11
15
  "private": false,
12
16
  "engines": {
13
17
  "node": ">=18"