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.
- package/README.md +44 -14
- package/bin/agentspend.js +269 -66
- 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
|
-
|
|
7
|
+
One-command setup:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
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
|
-
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Inside a repo:
|
|
14
17
|
|
|
15
18
|
```bash
|
|
16
|
-
|
|
19
|
+
costlayers init
|
|
20
|
+
costlayers scan
|
|
17
21
|
```
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
Launch Codex with CostLayers context:
|
|
20
24
|
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
-
|
|
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.
|
|
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
|
-
|
|
49
|
+
CostLayers ${VERSION}
|
|
45
50
|
|
|
46
51
|
Usage:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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("#
|
|
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 `
|
|
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 `#
|
|
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 \`
|
|
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
|
|
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 \`
|
|
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 =
|
|
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 `#
|
|
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 \`
|
|
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(`
|
|
373
|
-
process.stdout.write("Next:
|
|
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(`
|
|
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
|
-
|
|
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
|
|
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(`
|
|
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:
|
|
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("#
|
|
713
|
+
lines.push("# CostLayers Runtime Plan");
|
|
532
714
|
lines.push("");
|
|
533
|
-
lines.push(plan.plan_summary || "Use
|
|
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
|
|
567
|
-
|
|
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(
|
|
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(`
|
|
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:
|
|
792
|
+
process.stdout.write(`Report: costlayers gateway report\n`);
|
|
610
793
|
return;
|
|
611
794
|
}
|
|
612
795
|
if (action === "report") {
|
|
613
|
-
const
|
|
614
|
-
if (!
|
|
615
|
-
process.stderr.write(`${JSON.stringify(
|
|
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 =
|
|
619
|
-
process.stdout.write(`
|
|
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
|
|
622
|
-
process.stdout.write(`provider_avoided: ${summary
|
|
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(`
|
|
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:
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
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(`
|
|
687
|
-
process.stdout.write(`
|
|
688
|
-
process.stdout.write(`
|
|
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(`
|
|
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