costlayers 0.8.1 → 0.8.9

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.
Files changed (3) hide show
  1. package/README.md +44 -14
  2. package/bin/agentspend.js +269 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,37 +4,65 @@ CostLayers helps teams reduce wasted AI coding-agent spend by creating a compact
4
4
 
5
5
  ## Install
6
6
 
7
- Preview from the live server:
7
+ One-command setup:
8
8
 
9
9
  ```bash
10
- npx -y costlayers start --email you@example.com
10
+ cd your-repo
11
+ npx -y https://costlayers.com/costlayers-0.8.9.tgz start --email you@example.com
11
12
  ```
12
13
 
13
- After npm publish, the intended short command is:
14
+ ## Usage
15
+
16
+ Inside a repo:
14
17
 
15
18
  ```bash
16
- npx costlayers start --email you@example.com
19
+ costlayers init
20
+ costlayers scan
17
21
  ```
18
22
 
19
- ## Usage
23
+ Launch Codex with CostLayers context:
20
24
 
21
- Inside a repo:
25
+ ```bash
26
+ cd your-repo
27
+ npx -y https://costlayers.com/costlayers-0.8.9.tgz start --email you@example.com -- codex
28
+ ```
29
+
30
+ This gives Codex `.agentspend/repo-pack.md` and `.agentspend/runtime-plan.md`
31
+ so it can avoid repeated broad repo exploration. It also writes
32
+ `~/.codex/costlayers.config.toml` and runs Codex as
33
+ `codex --profile costlayers`, enabling gateway metering and redacted Codex token
34
+ telemetry for ChatGPT-login sessions.
35
+
36
+ To install only the Codex profile after signup:
22
37
 
23
38
  ```bash
24
- agentspend init
25
- agentspend scan
39
+ npx -y https://costlayers.com/costlayers-0.8.9.tgz codex-profile
40
+ codex --profile costlayers
26
41
  ```
27
42
 
28
43
  With the closed engine enabled:
29
44
 
30
45
  ```bash
31
- agentspend signup --email you@example.com
32
- agentspend gateway start --provider-url https://api.openai.com --mode reduce
33
- agentspend run -- codex
34
- agentspend gateway report
35
- agentspend dashboard
46
+ costlayers signup --email you@example.com
47
+ costlayers gateway start --provider-url https://api.openai.com --mode reduce
48
+ costlayers run -- codex
49
+ costlayers gateway report
50
+ costlayers dashboard
36
51
  ```
37
52
 
53
+ Metered dashboard savings only increase when model API traffic is routed through
54
+ the CostLayers gateway URL. Codex-metered dashboard savings increase when Codex
55
+ uses the CostLayers profile or emits token telemetry to the CostLayers meter.
56
+ For ChatGPT-login Codex, dollar savings are modeled from observed Codex token
57
+ events because the ChatGPT plan invoice is not an OpenAI Platform API invoice.
58
+
59
+ The hosted reducer defaults to quality-safe reduction:
60
+
61
+ - exact cache hits may skip repeated upstream calls
62
+ - certified compaction preserves the current user request
63
+ - prior context is compacted only when there is a safe structural boundary
64
+ - opaque single-message prompts are forwarded unchanged
65
+
38
66
  Output:
39
67
 
40
68
  - `.agentspend/repo-pack.md`
@@ -54,9 +82,11 @@ The first public version reduces waste by making agents start from a compact rep
54
82
  The report estimates:
55
83
 
56
84
  - baseline broad-read tokens
57
- - AgentSpend context-pack tokens
85
+ - CostLayers context-pack tokens
58
86
  - tokens avoided per repeated task
59
87
  - estimated cost avoided per 100 repeated tasks
88
+ - projected weekly and monthly savings at the chosen runs/week assumption
89
+ - before/after token reduction for the first scan
60
90
 
61
91
  No private internals are included in this package.
62
92
 
package/bin/agentspend.js CHANGED
@@ -6,9 +6,12 @@ const path = require("path");
6
6
  const crypto = require("crypto");
7
7
  const http = require("http");
8
8
  const https = require("https");
9
+ const os = require("os");
9
10
  const { spawnSync } = require("child_process");
10
11
 
11
- const VERSION = "0.8.1";
12
+ const VERSION = "0.8.9";
13
+ const DEFAULT_RUNS_PER_WEEK = 20;
14
+ const WEEKS_PER_MONTH = 4.33;
12
15
  const DEFAULT_EXCLUDES = new Set([
13
16
  ".git",
14
17
  ".hg",
@@ -28,6 +31,8 @@ const DEFAULT_EXCLUDES = new Set([
28
31
  ".ruff_cache",
29
32
  ".turbo",
30
33
  ".cache",
34
+ ".npm-cache",
35
+ ".pnpm-store",
31
36
  ".agentspend"
32
37
  ]);
33
38
 
@@ -41,26 +46,28 @@ const SOURCE_EXTENSIONS = new Set([
41
46
 
42
47
  function usage(exitCode = 0) {
43
48
  const text = `
44
- AgentSpend ${VERSION}
49
+ CostLayers ${VERSION}
45
50
 
46
51
  Usage:
47
- agentspend init [--repo <path>]
48
- agentspend scan [--repo <path>] [--price-per-1m <usd>] [--tasks <n>]
49
- agentspend start [--email <email>] [--provider-url <url>] [--mode measure|reduce] [-- <agent command>]
50
- agentspend signup [--email <email>] [--engine-url <url>]
51
- agentspend connect --engine-url <url> [--api-key <key>]
52
- agentspend gateway start [--provider-url <url>] [--api-key-env <name>] [--mode measure|reduce]
53
- agentspend gateway report
54
- agentspend gateway stop
55
- agentspend dashboard
56
- agentspend run [--repo <path>] -- <agent command>
57
- agentspend doctor
52
+ costlayers init [--repo <path>]
53
+ costlayers scan [--repo <path>] [--price-per-1m <usd>] [--tasks <n>] [--runs-per-week <n>]
54
+ costlayers start [--email <email>] [--provider-url <url>] [--mode measure|reduce] [--runs-per-week <n>] [-- <agent command>]
55
+ costlayers signup [--email <email>] [--engine-url <url>]
56
+ costlayers codex-profile [--repo <path>]
57
+ costlayers connect --engine-url <url> [--api-key <key>]
58
+ costlayers gateway start [--provider-url <url>] [--api-key-env <name>] [--mode measure|reduce]
59
+ costlayers gateway report
60
+ costlayers gateway stop
61
+ costlayers dashboard
62
+ costlayers run [--repo <path>] -- <agent command>
63
+ costlayers doctor
58
64
 
59
65
  Commands:
60
66
  init Create .agentspend config and agent instructions.
61
67
  scan Build repo context pack and savings report.
62
68
  start One-command setup, signup, gateway start, and optional agent run.
63
69
  signup Create a self-serve key and save connection settings.
70
+ codex-profile Write ~/.codex/costlayers.config.toml for metered Codex runs.
64
71
  connect Save closed-engine connection settings.
65
72
  gateway Manage the closed cost gateway.
66
73
  dashboard Print dashboard URL and account status.
@@ -99,6 +106,31 @@ function writeIfMissing(file, content) {
99
106
  if (!fs.existsSync(file)) fs.writeFileSync(file, content, "utf8");
100
107
  }
101
108
 
109
+ function isHomeDirectory(repo) {
110
+ try {
111
+ return path.resolve(repo) === path.resolve(os.homedir());
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ function guardRepoRoot(repo, args) {
118
+ if (!isHomeDirectory(repo) || args["allow-home"]) return;
119
+ process.stderr.write([
120
+ "CostLayers is about to use your home directory as the repo.",
121
+ `Repo detected: ${repo}`,
122
+ "",
123
+ "Run it inside a project folder instead:",
124
+ " cd path/to/your-repo",
125
+ " npx -y https://costlayers.com/costlayers-0.8.9.tgz start --email you@example.com",
126
+ "",
127
+ "Or pass --repo path/to/your-repo from anywhere.",
128
+ "If you really intend to scan your whole home directory, add --allow-home.",
129
+ ""
130
+ ].join("\n"));
131
+ process.exit(2);
132
+ }
133
+
102
134
  function sha256(text) {
103
135
  return crypto.createHash("sha256").update(text).digest("hex");
104
136
  }
@@ -205,7 +237,7 @@ function summarizeRepo(root) {
205
237
 
206
238
  function buildRepoPack(repo, summary) {
207
239
  const parts = [];
208
- parts.push("# AgentSpend Repo Pack");
240
+ parts.push("# CostLayers Repo Pack");
209
241
  parts.push("");
210
242
  parts.push("Read this before broad repo exploration. Use it to avoid repeatedly rediscovering project structure.");
211
243
  parts.push("");
@@ -241,20 +273,20 @@ function buildRepoPack(repo, summary) {
241
273
  parts.push("- Start with this pack before reading many files.");
242
274
  parts.push("- Prefer targeted reads of files listed above.");
243
275
  parts.push("- If a file hash is unchanged, do not reread it unless the task requires exact code.");
244
- parts.push("- Update this pack after major repo changes with `agentspend scan`.");
276
+ parts.push("- Update this pack after major repo changes with `costlayers scan`.");
245
277
  parts.push("");
246
278
  return parts.join("\n");
247
279
  }
248
280
 
249
281
  function buildInstructions() {
250
- return `# AgentSpend Agent Instructions
282
+ return `# CostLayers Agent Instructions
251
283
 
252
284
  Before broad repo exploration:
253
285
 
254
286
  1. Read .agentspend/repo-pack.md.
255
287
  2. Use the listed entry points, route hints, and symbol hints to target file reads.
256
288
  3. Avoid rereading unchanged large files unless exact code is required.
257
- 4. After major repo changes, ask the user to run \`agentspend scan\`.
289
+ 4. After major repo changes, ask the user to run \`costlayers scan\`.
258
290
 
259
291
  Goal: reduce repeated context spend while preserving answer quality.
260
292
  `;
@@ -263,7 +295,7 @@ Goal: reduce repeated context spend while preserving answer quality.
263
295
  function buildAgentsMd() {
264
296
  return `# Repository Agent Instructions
265
297
 
266
- This repo uses AgentSpend to reduce repeated AI coding-agent context spend.
298
+ This repo uses CostLayers to reduce repeated AI coding-agent context spend.
267
299
 
268
300
  Before broad exploration:
269
301
 
@@ -271,17 +303,24 @@ Before broad exploration:
271
303
  2. Read .agentspend/agent-instructions.md.
272
304
  3. Prefer targeted file reads based on the repo pack.
273
305
  4. Avoid rereading unchanged large files unless exact code is required.
274
- 5. After major repo changes, run or ask for \`agentspend scan\`.
306
+ 5. After major repo changes, run or ask for \`costlayers scan\`.
275
307
 
276
308
  `;
277
309
  }
278
310
 
279
- function buildReport(summary, repoPack, tasks, pricePer1m) {
311
+ function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek) {
280
312
  const packTokens = estimateTokens(repoPack);
281
- const broadReadTokens = summary.topFiles.slice(0, 20).reduce((acc, row) => acc + row.tokens, 0);
313
+ const broadReadTokens = Math.max(
314
+ summary.totalTokens,
315
+ summary.topFiles.slice(0, 20).reduce((acc, row) => acc + row.tokens, 0),
316
+ packTokens
317
+ );
282
318
  const avoidedPerTask = Math.max(0, broadReadTokens - packTokens);
283
319
  const avoidedTotal = avoidedPerTask * tasks;
284
320
  const savedUsd = avoidedTotal / 1_000_000 * pricePer1m;
321
+ const savedPerRunUsd = avoidedPerTask / 1_000_000 * pricePer1m;
322
+ const projectedWeeklyUsd = savedPerRunUsd * runsPerWeek;
323
+ const projectedMonthlyUsd = projectedWeeklyUsd * WEEKS_PER_MONTH;
285
324
  const reductionPct = broadReadTokens > 0 ? avoidedPerTask / broadReadTokens * 100 : 0;
286
325
  return {
287
326
  created_utc: new Date().toISOString(),
@@ -294,6 +333,9 @@ function buildReport(summary, repoPack, tasks, pricePer1m) {
294
333
  tokens_avoided_total: avoidedTotal,
295
334
  price_per_1m_input_tokens_usd: pricePer1m,
296
335
  estimated_usd_saved: Number(savedUsd.toFixed(4)),
336
+ projected_runs_per_week: runsPerWeek,
337
+ projected_weekly_savings_usd: Number(projectedWeeklyUsd.toFixed(4)),
338
+ projected_monthly_savings_usd: Number(projectedMonthlyUsd.toFixed(4)),
297
339
  estimated_reduction_percent: Number(reductionPct.toFixed(2)),
298
340
  largest_files: summary.topFiles.slice(0, 10).map((row) => ({
299
341
  path: row.rel,
@@ -307,10 +349,11 @@ function scanToFiles(repo, args) {
307
349
  const outDir = path.join(repo, ".agentspend");
308
350
  ensureDir(outDir);
309
351
  const tasks = Number(args.tasks || 100);
352
+ const runsPerWeek = Number(args["runs-per-week"] || DEFAULT_RUNS_PER_WEEK);
310
353
  const pricePer1m = Number(args["price-per-1m"] || 2.0);
311
354
  const summary = summarizeRepo(repo);
312
355
  const pack = buildRepoPack(repo, summary);
313
- const report = buildReport(summary, pack, tasks, pricePer1m);
356
+ const report = buildReport(summary, pack, tasks, pricePer1m, runsPerWeek);
314
357
  fs.writeFileSync(path.join(outDir, "repo-pack.md"), pack, "utf8");
315
358
  fs.writeFileSync(path.join(outDir, "agent-instructions.md"), buildInstructions(), "utf8");
316
359
  fs.writeFileSync(path.join(outDir, "savings-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8");
@@ -319,7 +362,7 @@ function scanToFiles(repo, args) {
319
362
  }
320
363
 
321
364
  function reportMarkdown(report) {
322
- return `# AgentSpend Savings Report
365
+ return `# CostLayers Savings Report
323
366
 
324
367
  Generated: ${report.created_utc}
325
368
 
@@ -334,12 +377,15 @@ Generated: ${report.created_utc}
334
377
  - Estimated tokens avoided total: ${report.tokens_avoided_total.toLocaleString()}
335
378
  - Estimated reduction: ${report.estimated_reduction_percent}%
336
379
  - Estimated saved: $${report.estimated_usd_saved}
380
+ - Projected runs/week: ${report.projected_runs_per_week}
381
+ - Projected weekly savings: $${report.projected_weekly_savings_usd}
382
+ - Projected monthly savings: $${report.projected_monthly_savings_usd}
337
383
 
338
384
  ## How Users Get Value
339
385
 
340
386
  1. Commit or keep \`.agentspend/repo-pack.md\` locally.
341
387
  2. Tell the coding agent to read it before broad repo exploration.
342
- 3. Re-run \`agentspend scan\` after major repo changes.
388
+ 3. Re-run \`costlayers scan\` after major repo changes.
343
389
  4. Compare provider usage before and after adopting this workflow.
344
390
 
345
391
  ## Largest Repeated Context Sources
@@ -369,18 +415,18 @@ function init(repo) {
369
415
  } else {
370
416
  process.stdout.write(`AGENTS.md already exists; snippet saved to ${path.join(outDir, "AGENTS_SNIPPET.md")}\n`);
371
417
  }
372
- process.stdout.write(`AgentSpend initialized in ${outDir}\n`);
373
- process.stdout.write("Next: agentspend scan\n");
418
+ process.stdout.write(`CostLayers initialized in ${outDir}\n`);
419
+ process.stdout.write("Next: costlayers scan\n");
374
420
  }
375
421
 
376
422
  function scan(repo, args) {
423
+ process.stdout.write(`Scanning repo: ${repo}\n`);
377
424
  const precomputed = scanToFiles(repo, args);
378
425
  const { outDir, report } = precomputed;
379
- process.stdout.write(`AgentSpend scan complete\n`);
426
+ process.stdout.write(`CostLayers scan complete\n`);
380
427
  process.stdout.write(`Repo pack: ${path.join(outDir, "repo-pack.md")}\n`);
381
428
  process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
382
- process.stdout.write(`Estimated tokens avoided per repeated task: ${report.tokens_avoided_per_repeated_task.toLocaleString()}\n`);
383
- process.stdout.write(`Estimated saved per ${report.repeated_tasks_modeled} repeated tasks: $${report.estimated_usd_saved}\n`);
429
+ printSavingsSummary(report);
384
430
  }
385
431
 
386
432
  function connectEngine(repo, args) {
@@ -397,7 +443,7 @@ function connectEngine(repo, args) {
397
443
  connected_utc: new Date().toISOString()
398
444
  };
399
445
  fs.writeFileSync(path.join(outDir, "connection.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
400
- process.stdout.write(`Connected AgentSpend engine: ${config.engine_url}\n`);
446
+ process.stdout.write(`Connected CostLayers engine: ${config.engine_url}\n`);
401
447
  }
402
448
 
403
449
  function loadConnection(repo, args) {
@@ -427,13 +473,122 @@ function defaultPublicGatewayUrl(engineUrl, apiKey) {
427
473
  }
428
474
  }
429
475
 
476
+ function formatUsd(value) {
477
+ const amount = Number(value || 0);
478
+ if (amount < 1) return `$${amount.toFixed(4)}`;
479
+ return `$${amount.toFixed(2)}`;
480
+ }
481
+
482
+ function formatInt(value) {
483
+ return Number(value || 0).toLocaleString();
484
+ }
485
+
486
+ function savingsProjection(report) {
487
+ const tasks = Math.max(1, Number(report.repeated_tasks_modeled || 0));
488
+ const runsPerWeek = Math.max(1, Number(report.projected_runs_per_week || DEFAULT_RUNS_PER_WEEK));
489
+ const savedPerRunUsd = Number(report.estimated_usd_saved || 0) / tasks;
490
+ const weeklyUsd = savedPerRunUsd * runsPerWeek;
491
+ const monthlyUsd = weeklyUsd * WEEKS_PER_MONTH;
492
+ return {
493
+ tasks,
494
+ runsPerWeek,
495
+ savedPerRunUsd,
496
+ weeklyUsd,
497
+ monthlyUsd
498
+ };
499
+ }
500
+
501
+ function dashboardUrlFromConnection(connection) {
502
+ return (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
503
+ }
504
+
505
+ function printSavingsSummary(report) {
506
+ const projection = savingsProjection(report);
507
+ const avoided = Number(report.tokens_avoided_per_repeated_task || 0);
508
+ process.stdout.write(`\n${avoided > 0 ? "CostLayers found repeated context waste" : "CostLayers built your repo context pack"}\n`);
509
+ process.stdout.write(` Tokens avoided per repeated task: ${formatInt(report.tokens_avoided_per_repeated_task)}\n`);
510
+ if (avoided > 0) {
511
+ process.stdout.write(` Before vs after context: ${formatInt(report.baseline_broad_read_tokens)} -> ${formatInt(report.context_pack_tokens)} tokens (${report.estimated_reduction_percent}% less)\n`);
512
+ } else {
513
+ process.stdout.write(` No repeated-context reduction found yet for this small/simple repo.\n`);
514
+ process.stdout.write(` Context pack size: ${formatInt(report.context_pack_tokens)} tokens from ${formatInt(report.source_tokens_indexed)} indexed source tokens\n`);
515
+ }
516
+ process.stdout.write(` Modeled savings per ${formatInt(report.repeated_tasks_modeled)} repeated tasks: ${formatUsd(report.estimated_usd_saved)}\n`);
517
+ process.stdout.write(` Projected at ${formatInt(projection.runsPerWeek)} agent runs/week: ${formatUsd(projection.weeklyUsd)}/week, ${formatUsd(projection.monthlyUsd)}/month\n`);
518
+ process.stdout.write(` Source tokens indexed: ${formatInt(report.source_tokens_indexed)}\n`);
519
+ process.stdout.write(` Compact repo pack: ${formatInt(report.context_pack_tokens)} tokens\n`);
520
+ }
521
+
522
+ function codexHomeDir() {
523
+ return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
524
+ }
525
+
526
+ function profileTomlString(connection) {
527
+ const gateway = String(connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace(/\/+$/, "");
528
+ const baseUrl = `${gateway}/v1`;
529
+ const engineUrl = String(connection.engine_url || "https://costlayers.com/engine").replace(/\/+$/, "");
530
+ return [
531
+ "# Generated by CostLayers. This profile routes Codex through the CostLayers meter.",
532
+ "# Keep this file private because it contains your keyed CostLayers endpoint.",
533
+ "",
534
+ 'model_provider = "costlayers"',
535
+ "",
536
+ "[model_providers.costlayers]",
537
+ 'name = "CostLayers"',
538
+ `base_url = ${JSON.stringify(baseUrl)}`,
539
+ 'wire_api = "responses"',
540
+ "requires_openai_auth = true",
541
+ "",
542
+ "[otel]",
543
+ 'environment = "costlayers"',
544
+ 'exporter = "otlp-http"',
545
+ "log_user_prompt = false",
546
+ "",
547
+ '[otel.exporter."otlp-http"]',
548
+ `endpoint = ${JSON.stringify(`${engineUrl}/v1/codex-meter`)}`,
549
+ 'protocol = "json"',
550
+ "",
551
+ '[otel.exporter."otlp-http".headers]',
552
+ `"x-costlayers-key" = ${JSON.stringify(connection.api_key || "")}`,
553
+ ""
554
+ ].join("\n");
555
+ }
556
+
557
+ function writeCodexProfile(connection) {
558
+ const dir = codexHomeDir();
559
+ ensureDir(dir);
560
+ const file = path.join(dir, "costlayers.config.toml");
561
+ fs.writeFileSync(file, profileTomlString(connection), "utf8");
562
+ return file;
563
+ }
564
+
565
+ function commandName(command) {
566
+ const first = String(command[0] || "");
567
+ return path.basename(first).toLowerCase().replace(/\.(cmd|exe|bat|ps1)$/i, "");
568
+ }
569
+
570
+ function isCodexCommand(command) {
571
+ return commandName(command) === "codex";
572
+ }
573
+
574
+ function commandHasProfile(command) {
575
+ return command.some((item) => item === "--profile" || String(item).startsWith("--profile="));
576
+ }
577
+
578
+ function withCostLayersCodexProfile(command) {
579
+ if (!isCodexCommand(command) || commandHasProfile(command)) return command;
580
+ return [command[0], "--profile", "costlayers", ...command.slice(1)];
581
+ }
582
+
430
583
  async function signupConnection(repo, args) {
431
584
  const engineUrl = String(args["engine-url"] || "https://costlayers.com/engine").replace(/\/+$/, "");
585
+ const savingsReport = readJsonIfExists(path.join(repo, ".agentspend", "savings-report.json"));
432
586
  const payload = {
433
587
  email: args.email || "",
434
588
  label: args.label || path.basename(repo),
435
589
  public_host: engineUrl.replace(/\/engine$/, "")
436
590
  };
591
+ if (savingsReport) payload.savings_report = savingsReport;
437
592
  const result = await postJson(`${engineUrl}/v1/register`, payload, null);
438
593
  if (!result.ok || !result.api_key) {
439
594
  process.stderr.write(`Signup failed: ${JSON.stringify(result, null, 2)}\n`);
@@ -460,11 +615,24 @@ async function ensureConnection(repo, args) {
460
615
 
461
616
  async function signup(repo, args) {
462
617
  const connection = await signupConnection(repo, args);
463
- process.stdout.write(`AgentSpend self-serve key created\n`);
618
+ process.stdout.write(`CostLayers self-serve key created\n`);
464
619
  process.stdout.write(`Engine: ${connection.engine_url}\n`);
465
620
  process.stdout.write(`Gateway: ${connection.gateway_url}\n`);
621
+ process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
622
+ process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
466
623
  process.stdout.write(`Plan: free beta\n`);
467
- process.stdout.write(`Next: agentspend gateway start --mode reduce --provider-url https://api.openai.com\n`);
624
+ process.stdout.write(`Next: costlayers gateway start --mode reduce --provider-url https://api.openai.com\n`);
625
+ }
626
+
627
+ async function codexProfile(repo, args) {
628
+ const connection = await ensureConnection(repo, args);
629
+ const profilePath = writeCodexProfile(connection);
630
+ process.stdout.write(`CostLayers Codex profile installed\n`);
631
+ process.stdout.write(`Profile: ${profilePath}\n`);
632
+ process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
633
+ process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
634
+ process.stdout.write(`Run: codex --profile costlayers\n`);
635
+ process.stdout.write(`Meter endpoint: ${connection.engine_url.replace(/\/+$/, "")}/v1/codex-meter\n`);
468
636
  }
469
637
 
470
638
  function postJson(urlString, payload, apiKey) {
@@ -526,11 +694,25 @@ function buildLocalPlan(report) {
526
694
  };
527
695
  }
528
696
 
697
+ async function fetchEnginePlan(connection, repo, pack, report) {
698
+ if (!connection || !connection.engine_url) return null;
699
+ const payload = {
700
+ version: VERSION,
701
+ repo_name: path.basename(repo),
702
+ repo_pack_sha256: sha256(pack),
703
+ repo_pack_preview: pack.slice(0, 12000),
704
+ savings_report: report
705
+ };
706
+ const plan = await postJson(`${connection.engine_url}/v1/plan`, payload, connection.api_key);
707
+ plan.mode = plan.mode || "closed-engine";
708
+ return plan;
709
+ }
710
+
529
711
  function writeRuntimePrompt(outDir, plan) {
530
712
  const lines = [];
531
- lines.push("# AgentSpend Runtime Plan");
713
+ lines.push("# CostLayers Runtime Plan");
532
714
  lines.push("");
533
- lines.push(plan.plan_summary || "Use AgentSpend repo context before broad exploration.");
715
+ lines.push(plan.plan_summary || "Use CostLayers repo context before broad exploration.");
534
716
  lines.push("");
535
717
  lines.push("## Instructions");
536
718
  for (const item of plan.runtime_instructions || []) lines.push(`- ${item}`);
@@ -555,29 +737,30 @@ async function runAgent(repo, args, argv, options = {}) {
555
737
  const connection = readJsonIfExists(path.join(outDir, "connection.json"));
556
738
  let plan = buildLocalPlan(report);
557
739
  if (connection && connection.engine_url) {
558
- const payload = {
559
- version: VERSION,
560
- repo_name: path.basename(repo),
561
- repo_pack_sha256: sha256(pack),
562
- repo_pack_preview: pack.slice(0, 12000),
563
- savings_report: report
564
- };
565
740
  try {
566
- plan = await postJson(`${connection.engine_url}/v1/plan`, payload, connection.api_key);
567
- plan.mode = plan.mode || "closed-engine";
568
- process.stdout.write(`Fetched AgentSpend engine plan\n`);
741
+ plan = await fetchEnginePlan(connection, repo, pack, report);
742
+ process.stdout.write(`Fetched CostLayers engine plan\n`);
569
743
  } catch (err) {
570
744
  process.stderr.write(`Engine unavailable; falling back to local plan: ${err.message}\n`);
571
745
  }
572
746
  }
573
747
  writeRuntimePrompt(outDir, plan);
574
748
  process.stdout.write(`Runtime plan: ${path.join(outDir, "runtime-plan.md")}\n`);
749
+ let commandToRun = command;
750
+ if (connection && connection.engine_url && isCodexCommand(command)) {
751
+ const profilePath = writeCodexProfile(connection);
752
+ commandToRun = withCostLayersCodexProfile(command);
753
+ process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
754
+ process.stdout.write(`Codex metering enabled: ${commandToRun.join(" ")}\n`);
755
+ process.stdout.write(`Savings dashboard: ${dashboardUrlFromConnection(connection)}\n`);
756
+ process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
757
+ }
575
758
  const env = {
576
759
  ...process.env,
577
760
  AGENTSPEND_REPO_PACK: path.join(outDir, "repo-pack.md"),
578
761
  AGENTSPEND_RUNTIME_PLAN: path.join(outDir, "runtime-plan.md")
579
762
  };
580
- const result = spawnSync(command[0], command.slice(1), {
763
+ const result = spawnSync(commandToRun[0], commandToRun.slice(1), {
581
764
  cwd: repo,
582
765
  env,
583
766
  stdio: "inherit",
@@ -604,26 +787,28 @@ async function gateway(repo, args) {
604
787
  process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
605
788
  process.exit(1);
606
789
  }
607
- process.stdout.write(`AgentSpend gateway ready: ${result.base_url}\n`);
790
+ process.stdout.write(`CostLayers gateway ready: ${result.base_url}\n`);
608
791
  process.stdout.write(`Set your OpenAI-compatible base URL to: ${result.base_url}\n`);
609
- process.stdout.write(`Report: agentspend gateway report\n`);
792
+ process.stdout.write(`Report: costlayers gateway report\n`);
610
793
  return;
611
794
  }
612
795
  if (action === "report") {
613
- const result = await postJson(`${connection.engine_url}/v1/gateway/report`, { port: Number(args.port || 8788) }, connection.api_key);
614
- if (!result.ok) {
615
- process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
796
+ const status = await postJson(`${connection.engine_url}/v1/me`, {}, connection.api_key);
797
+ if (!status.ok) {
798
+ process.stderr.write(`${JSON.stringify(status, null, 2)}\n`);
616
799
  process.exit(1);
617
800
  }
618
- const summary = result.summary || {};
619
- process.stdout.write(`AgentSpend Gateway Report\n`);
801
+ const summary = status.gateway_summary || {};
802
+ process.stdout.write(`CostLayers Gateway Report\n`);
803
+ process.stdout.write(`scope: current user key\n`);
620
804
  process.stdout.write(`requests: ${summary.request_count || 0}\n`);
621
- process.stdout.write(`provider_called: ${summary["up" + "stream_called"] || 0}\n`);
622
- process.stdout.write(`provider_avoided: ${summary["up" + "stream_avoided"] || 0}\n`);
805
+ process.stdout.write(`provider_called: ${summary.provider_called || 0}\n`);
806
+ process.stdout.write(`provider_avoided: ${summary.provider_avoided || 0}\n`);
623
807
  process.stdout.write(`baseline_cost_usd: ${summary.baseline_cost_usd || 0}\n`);
624
808
  process.stdout.write(`actual_cost_usd: ${summary.actual_cost_usd || 0}\n`);
625
809
  process.stdout.write(`saved_cost_usd: ${summary.saved_cost_usd || 0}\n`);
626
- process.stdout.write(`report_path: ${result.report_path || ""}\n`);
810
+ process.stdout.write(`gateway_authenticated_actions: ${status.gateway_request_count || 0}\n`);
811
+ process.stdout.write(`dashboard: ${dashboardUrlFromConnection(connection)}\n`);
627
812
  return;
628
813
  }
629
814
  if (action === "stop") {
@@ -631,7 +816,7 @@ async function gateway(repo, args) {
631
816
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
632
817
  return;
633
818
  }
634
- process.stderr.write("Usage: agentspend gateway start|report|stop\n");
819
+ process.stderr.write("Usage: costlayers gateway start|report|stop\n");
635
820
  process.exit(2);
636
821
  }
637
822
 
@@ -639,11 +824,15 @@ async function dashboard(repo, args) {
639
824
  const connection = loadConnection(repo, args);
640
825
  const status = await postJson(`${connection.engine_url}/v1/me`, {}, connection.api_key);
641
826
  const dashboardUrl = (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
642
- process.stdout.write(`AgentSpend Dashboard\n`);
827
+ process.stdout.write(`CostLayers Dashboard\n`);
643
828
  process.stdout.write(`URL: ${dashboardUrl}\n`);
644
829
  process.stdout.write(`status: ${status.status || "unknown"}\n`);
645
830
  process.stdout.write(`plan: ${status.free_beta ? "free beta" : "metered"}\n`);
646
831
  process.stdout.write(`metered_savings_usd: ${status.metered_savings_usd ?? ""}\n`);
832
+ process.stdout.write(`codex_metered_savings_usd: ${status.codex_metered_savings_usd ?? ""}\n`);
833
+ process.stdout.write(`codex_meter_total_tokens: ${status.codex_meter_total_tokens ?? 0}\n`);
834
+ process.stdout.write(`codex_context_savings_usd: ${status.context_savings_usd ?? ""}\n`);
835
+ process.stdout.write(`context_tokens_avoided_per_task: ${status.context_tokens_avoided_per_task ?? 0}\n`);
647
836
  process.stdout.write(`gateway_requests: ${status.gateway_request_count ?? 0}\n`);
648
837
  process.stdout.write(`blocked_requests: ${status.blocked_request_count ?? 0}\n`);
649
838
  process.stdout.write(`rate_limit_per_minute: ${status.rate_limit_per_minute ?? ""}\n`);
@@ -653,12 +842,20 @@ async function start(repo, args, argv) {
653
842
  const dash = argv.indexOf("--");
654
843
  const command = dash >= 0 ? argv.slice(dash + 1) : [];
655
844
  init(repo);
845
+ process.stdout.write(`Scanning repo: ${repo}\n`);
656
846
  const precomputed = scanToFiles(repo, args);
657
- const { outDir, report } = precomputed;
658
- process.stdout.write(`AgentSpend scan complete\n`);
847
+ const { outDir, pack, report } = precomputed;
848
+ process.stdout.write(`CostLayers scan complete\n`);
659
849
  process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
850
+ printSavingsSummary(report);
660
851
  const connection = await ensureConnection(repo, args);
661
- process.stdout.write(`AgentSpend connection ready\n`);
852
+ try {
853
+ await fetchEnginePlan(connection, repo, pack, report);
854
+ process.stdout.write(`Dashboard synced with first-run savings\n`);
855
+ } catch (err) {
856
+ process.stderr.write(`Dashboard sync delayed; local report is still available: ${err.message}\n`);
857
+ }
858
+ process.stdout.write(`CostLayers connection ready\n`);
662
859
  const providerUrl = typeof args["provider-url"] === "string" ? args["provider-url"] : "https://api.openai.com";
663
860
  const payload = {
664
861
  host: args.host || "127.0.0.1",
@@ -674,22 +871,26 @@ async function start(repo, args, argv) {
674
871
  process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
675
872
  process.exit(1);
676
873
  }
677
- process.stdout.write(`AgentSpend gateway ready: ${result.base_url}\n`);
874
+ process.stdout.write(`CostLayers gateway ready: ${result.base_url}\n`);
678
875
  process.stdout.write(`OpenAI-compatible base URL: ${result.base_url}\n`);
876
+ process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
877
+ process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
679
878
  process.stdout.write(`Plan: free beta\n`);
680
- process.stdout.write(`Estimated saved per ${report.repeated_tasks_modeled} repeated tasks: $${report.estimated_usd_saved}\n`);
879
+ const profilePath = writeCodexProfile(connection);
880
+ process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
681
881
  if (command.length > 0) {
682
882
  return runAgent(repo, args, argv, { skipSetup: true, precomputed });
683
883
  }
684
884
  process.stdout.write(`\nNext options:\n`);
685
885
  process.stdout.write(` Use gateway URL in your model client: ${result.base_url}\n`);
686
- process.stdout.write(` Or run an agent: npx -y costlayers start -- codex\n`);
687
- process.stdout.write(` View savings: npx -y costlayers gateway report\n`);
688
- process.stdout.write(` Dashboard: npx -y costlayers dashboard\n`);
886
+ process.stdout.write(` Run Codex metered: npx -y https://costlayers.com/costlayers-0.8.9.tgz start --email you@example.com -- codex\n`);
887
+ process.stdout.write(` Or run Codex directly: codex --profile costlayers\n`);
888
+ process.stdout.write(` View savings: npx -y https://costlayers.com/costlayers-0.8.9.tgz gateway report\n`);
889
+ process.stdout.write(` Dashboard: npx -y https://costlayers.com/costlayers-0.8.9.tgz dashboard\n`);
689
890
  }
690
891
 
691
892
  function doctor() {
692
- process.stdout.write(`AgentSpend ${VERSION}\n`);
893
+ process.stdout.write(`CostLayers ${VERSION}\n`);
693
894
  process.stdout.write(`Node ${process.version}\n`);
694
895
  process.stdout.write("Status: ok\n");
695
896
  }
@@ -700,11 +901,13 @@ function main() {
700
901
  const cmd = args._[0];
701
902
  if (!cmd || args.help || args.h) usage(0);
702
903
  const repo = path.resolve(String(args.repo || process.cwd()));
904
+ if (["init", "scan", "start", "run", "codex-profile"].includes(cmd)) guardRepoRoot(repo, args);
703
905
  if (cmd === "doctor") return doctor();
704
906
  if (cmd === "init") return init(repo);
705
907
  if (cmd === "scan") return scan(repo, args);
706
908
  if (cmd === "start") return start(repo, args, rawArgv);
707
909
  if (cmd === "signup") return signup(repo, args);
910
+ if (cmd === "codex-profile") return codexProfile(repo, args);
708
911
  if (cmd === "connect") return connectEngine(repo, args);
709
912
  if (cmd === "gateway") return gateway(repo, args);
710
913
  if (cmd === "dashboard") return dashboard(repo, args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "costlayers",
3
- "version": "0.8.1",
3
+ "version": "0.8.9",
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",