costlayers 0.8.17 → 0.8.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -9
- package/bin/agentspend.js +739 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# CostLayers CLI
|
|
2
2
|
|
|
3
|
-
CostLayers
|
|
3
|
+
CostLayers is an AI spend control layer for coding-agent teams. It starts with a free audit, finds repeated context waste, gives API users invoice-mode savings, and gives ChatGPT-login Codex users a usage-stretch meter that shows how much repeated context was avoided.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Run a free AI spend audit:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
cd your-repo
|
|
11
|
-
npx -y costlayers
|
|
11
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz audit --monthly-spend 10000
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Use your real monthly AI spend if you have it. Omit `--monthly-spend` to get a repo-only audit. The audit writes `.agentspend/ai-spend-audit.md` and labels scan estimates separately from verified invoice savings. After the audit, `costlayers dashboard` shows the local result even before signup.
|
|
15
15
|
|
|
16
16
|
Run a one-command API savings test:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
export OPENAI_API_KEY=sk-proj-...
|
|
20
|
-
npx -y costlayers
|
|
20
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz test --email you@example.com
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
@@ -25,7 +25,13 @@ npx -y costlayers@latest test --email you@example.com
|
|
|
25
25
|
Inside a repo:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
npx -y costlayers
|
|
28
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz audit --monthly-spend 10000
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then run Codex with CostLayers:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz codex --email you@example.com --chatgpt
|
|
29
35
|
```
|
|
30
36
|
|
|
31
37
|
This gives Codex `.agentspend/repo-pack.md` and `.agentspend/runtime-plan.md`
|
|
@@ -45,7 +51,7 @@ API write permission:
|
|
|
45
51
|
|
|
46
52
|
```bash
|
|
47
53
|
export OPENAI_API_KEY=sk-proj-...
|
|
48
|
-
npx -y costlayers
|
|
54
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz codex --email you@example.com --api
|
|
49
55
|
```
|
|
50
56
|
|
|
51
57
|
ChatGPT-login Codex can be metered, but it does not create per-request OpenAI
|
|
@@ -56,12 +62,13 @@ Platform invoice savings because it is not billed through your Platform API key.
|
|
|
56
62
|
- ChatGPT-login Codex: use `costlayers codex --email you@example.com --chatgpt` to reduce repeated repo context and stretch usage limits.
|
|
57
63
|
- OpenAI Platform API billing: set `OPENAI_API_KEY`, then use `costlayers codex --email you@example.com --api` for invoice-backed savings.
|
|
58
64
|
- Savings proof: set `OPENAI_API_KEY`, then run `costlayers test --email you@example.com`.
|
|
65
|
+
- Free audit: run `costlayers audit --monthly-spend <usd>` before changing a workflow.
|
|
59
66
|
- Other OpenAI-compatible clients: point the client at the CostLayers gateway URL and check `costlayers gateway report`.
|
|
60
67
|
|
|
61
68
|
To install only the Codex profile after signup:
|
|
62
69
|
|
|
63
70
|
```bash
|
|
64
|
-
npx -y costlayers
|
|
71
|
+
npx -y https://costlayers.com/costlayers-0.8.27.tgz codex-profile
|
|
65
72
|
codex --profile costlayers
|
|
66
73
|
```
|
|
67
74
|
|
|
@@ -88,9 +95,13 @@ The hosted reducer defaults to quality-safe reduction:
|
|
|
88
95
|
- certified compaction preserves the current user request
|
|
89
96
|
- prior context is compacted only when there is a safe structural boundary
|
|
90
97
|
- opaque single-message prompts are forwarded unchanged
|
|
98
|
+
- hosted raw provider-response caching is off by default; receipts use hashes,
|
|
99
|
+
token counts, costs, timestamps, and quality labels
|
|
91
100
|
|
|
92
101
|
Output:
|
|
93
102
|
|
|
103
|
+
- `.agentspend/ai-spend-audit.md`
|
|
104
|
+
- `.agentspend/ai-spend-audit.json`
|
|
94
105
|
- `.agentspend/repo-pack.md`
|
|
95
106
|
- `.agentspend/savings-report.md`
|
|
96
107
|
- `.agentspend/savings-report.json`
|
|
@@ -118,4 +129,12 @@ No private internals are included in this package.
|
|
|
118
129
|
|
|
119
130
|
## Closed Engine
|
|
120
131
|
|
|
121
|
-
The npm package is a controller. The stronger reduction engine and cost gateway run separately and are not shipped in the public package.
|
|
132
|
+
The npm package is a controller for the CostLayers optimization layer. The stronger reduction engine and cost gateway run separately and are not shipped in the public package.
|
|
133
|
+
|
|
134
|
+
## Privacy Default
|
|
135
|
+
|
|
136
|
+
The CLI scans source locally. When connected, it sends savings reports, artifact
|
|
137
|
+
hashes, and usage metadata so the dashboard can show found waste and metered
|
|
138
|
+
savings. It does not send the repo-pack preview by default. ChatGPT-login Codex
|
|
139
|
+
mode does not route model calls through CostLayers. API invoice mode routes
|
|
140
|
+
requests through the hosted optimization layer only when you pass `--api`.
|
package/bin/agentspend.js
CHANGED
|
@@ -9,8 +9,16 @@ const https = require("https");
|
|
|
9
9
|
const os = require("os");
|
|
10
10
|
const { spawnSync } = require("child_process");
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
function packageVersion() {
|
|
13
|
+
try {
|
|
14
|
+
return require(path.join(__dirname, "..", "package.json")).version || "0.8.27";
|
|
15
|
+
} catch (_err) {
|
|
16
|
+
return "0.8.27";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const VERSION = packageVersion();
|
|
21
|
+
const INSTALL_SPEC = process.env.COSTLAYERS_INSTALL_SPEC || `https://costlayers.com/costlayers-${VERSION}.tgz`;
|
|
14
22
|
const DEFAULT_RUNS_PER_WEEK = 20;
|
|
15
23
|
const WEEKS_PER_MONTH = 4.33;
|
|
16
24
|
const DEFAULT_EXCLUDES = new Set([
|
|
@@ -45,11 +53,14 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
45
53
|
".md", ".mdx", ".json", ".yaml", ".yml", ".toml", ".sql"
|
|
46
54
|
]);
|
|
47
55
|
|
|
56
|
+
const SEMANTIC_SLICE_LIMIT_PER_AREA = 24;
|
|
57
|
+
|
|
48
58
|
function usage(exitCode = 0) {
|
|
49
59
|
const text = `
|
|
50
60
|
CostLayers ${VERSION}
|
|
51
61
|
|
|
52
62
|
Usage:
|
|
63
|
+
costlayers audit [--email <email>] [--monthly-spend <usd>] [--usage-file <path>]
|
|
53
64
|
costlayers codex [--email <email>] [--chatgpt|--api] [-- <codex args>]
|
|
54
65
|
costlayers test [--email <email>] [--prompt <text>]
|
|
55
66
|
costlayers init [--repo <path>]
|
|
@@ -66,6 +77,7 @@ Usage:
|
|
|
66
77
|
costlayers doctor
|
|
67
78
|
|
|
68
79
|
Commands:
|
|
80
|
+
audit Run a free AI spend audit: repo context waste plus optional monthly spend/imported usage.
|
|
69
81
|
codex Start Codex with CostLayers. Defaults to ChatGPT-login mode unless --api is passed.
|
|
70
82
|
test Run a safe read-only API invoice-mode Codex task and print the CostLayers savings report.
|
|
71
83
|
init Create .agentspend config and agent instructions.
|
|
@@ -185,10 +197,10 @@ function repoConnectionPath(repo) {
|
|
|
185
197
|
return path.join(repo, ".agentspend", "connection.json");
|
|
186
198
|
}
|
|
187
199
|
|
|
188
|
-
function
|
|
200
|
+
function ensureCostLayersGitignore(outDir) {
|
|
189
201
|
ensureDir(outDir);
|
|
190
202
|
const file = path.join(outDir, ".gitignore");
|
|
191
|
-
const required = ["connection.json", "*.secret.json", "gateway-key.txt"];
|
|
203
|
+
const required = ["connection.json", "*.secret.json", "gateway-key.txt", "local-cache.json"];
|
|
192
204
|
let current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
193
205
|
const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
194
206
|
let changed = false;
|
|
@@ -229,7 +241,7 @@ function writePrivateJson(file, payload) {
|
|
|
229
241
|
function saveConnection(repo, connection) {
|
|
230
242
|
const outDir = path.join(repo, ".agentspend");
|
|
231
243
|
ensureDir(outDir);
|
|
232
|
-
|
|
244
|
+
ensureCostLayersGitignore(outDir);
|
|
233
245
|
const connectionId = connectionIdFor(repo, connection);
|
|
234
246
|
const full = {
|
|
235
247
|
...connection,
|
|
@@ -260,6 +272,12 @@ function estimateTokens(text) {
|
|
|
260
272
|
return Math.ceil(String(text || "").length / 4);
|
|
261
273
|
}
|
|
262
274
|
|
|
275
|
+
function stableJson(value) {
|
|
276
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
277
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
278
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
263
281
|
function walkFiles(root) {
|
|
264
282
|
const out = [];
|
|
265
283
|
function walk(dir) {
|
|
@@ -383,6 +401,7 @@ function buildRepoPack(repo, summary) {
|
|
|
383
401
|
parts.push("");
|
|
384
402
|
parts.push("## Agent Operating Rule");
|
|
385
403
|
parts.push("- Start with this pack before reading many files.");
|
|
404
|
+
parts.push("- Use .agentspend/semantic-slices.md for route and symbol facts before opening broad source context.");
|
|
386
405
|
parts.push("- Prefer targeted reads of files listed above.");
|
|
387
406
|
parts.push("- If a file hash is unchanged, do not reread it unless the task requires exact code.");
|
|
388
407
|
parts.push("- Update this pack after major repo changes with `costlayers scan`.");
|
|
@@ -390,15 +409,194 @@ function buildRepoPack(repo, summary) {
|
|
|
390
409
|
return parts.join("\n");
|
|
391
410
|
}
|
|
392
411
|
|
|
412
|
+
|
|
413
|
+
function semanticArea(row) {
|
|
414
|
+
const rel = String(row.rel || "").toLowerCase();
|
|
415
|
+
const signals = row.signals || {};
|
|
416
|
+
if ((signals.routeHints || []).length > 0) return "entrypoints-routes";
|
|
417
|
+
if (/(^|\/)(__tests__|test|tests|spec|specs)(\/|$)/.test(rel) || /\.(test|spec)\.[^.]+$/.test(rel)) return "tests";
|
|
418
|
+
if (/(^|\/)(package\.json|pyproject\.toml|cargo\.toml|go\.mod|requirements[^/]*\.txt|pom\.xml|build\.gradle|dockerfile|compose\.ya?ml|tsconfig\.json|vite\.config|next\.config|nuxt\.config)/.test(rel)) return "config-build";
|
|
419
|
+
if (/\.(md|mdx)$/.test(rel)) return "docs";
|
|
420
|
+
if (/(^|\/)(schema|schemas|model|models|migration|migrations|db|database)(\/|$)/.test(rel) || /\.sql$/.test(rel)) return "data-model";
|
|
421
|
+
if (/(^|\/)(api|server|service|services|controller|controllers|handler|handlers)(\/|$)/.test(rel)) return "services";
|
|
422
|
+
if (/(^|\/)(component|components|page|pages|app|ui|view|views)(\/|$)/.test(rel)) return "ui";
|
|
423
|
+
if (/(^|\/)(cli|bin|cmd|command|commands)(\/|$)/.test(rel)) return "cli-tools";
|
|
424
|
+
return "core";
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function semanticAreaTitle(area) {
|
|
428
|
+
return ({
|
|
429
|
+
"entrypoints-routes": "Entry Points And Routes",
|
|
430
|
+
"config-build": "Config And Build",
|
|
431
|
+
"data-model": "Data Model",
|
|
432
|
+
"cli-tools": "CLI And Tools",
|
|
433
|
+
services: "Services",
|
|
434
|
+
tests: "Tests",
|
|
435
|
+
docs: "Docs",
|
|
436
|
+
ui: "UI",
|
|
437
|
+
core: "Core"
|
|
438
|
+
})[area] || area;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function semanticSymbolName(definitionLine) {
|
|
442
|
+
const text = String(definitionLine || "").replace(/^\d+:\s*/, "").trim();
|
|
443
|
+
const patterns = [
|
|
444
|
+
/(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const|let|var)\s+([A-Za-z0-9_$]+)/,
|
|
445
|
+
/(?:def|class)\s+([A-Za-z0-9_]+)/,
|
|
446
|
+
/func\s+(?:\([^)]+\)\s*)?([A-Za-z0-9_]+)/
|
|
447
|
+
];
|
|
448
|
+
for (const pattern of patterns) {
|
|
449
|
+
const match = text.match(pattern);
|
|
450
|
+
if (match) return match[1];
|
|
451
|
+
}
|
|
452
|
+
return text.slice(0, 80);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function semanticFileFact(row) {
|
|
456
|
+
const signals = row.signals || {};
|
|
457
|
+
const symbols = (signals.defs || []).map(semanticSymbolName).filter(Boolean).slice(0, 12);
|
|
458
|
+
const routes = (signals.routeHints || []).slice(0, 8);
|
|
459
|
+
const imports = (signals.imports || []).slice(0, 6);
|
|
460
|
+
return {
|
|
461
|
+
path: row.rel,
|
|
462
|
+
area: semanticArea(row),
|
|
463
|
+
hash: row.hash,
|
|
464
|
+
tokens: row.tokens,
|
|
465
|
+
lines: signals.lineCount || 0,
|
|
466
|
+
symbols,
|
|
467
|
+
routes,
|
|
468
|
+
imports
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function semanticFactCount(fact) {
|
|
473
|
+
return 1 + fact.symbols.length + fact.routes.length + Math.min(fact.imports.length, 3);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function semanticFactScore(fact) {
|
|
477
|
+
return fact.routes.length * 50 + fact.symbols.length * 8 + fact.imports.length * 2 + Math.min(fact.tokens / 1000, 20);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function buildSemanticSlices(repo, summary) {
|
|
481
|
+
const facts = summary.files.map(semanticFileFact);
|
|
482
|
+
const groups = new Map();
|
|
483
|
+
for (const fact of facts) {
|
|
484
|
+
if (!groups.has(fact.area)) groups.set(fact.area, []);
|
|
485
|
+
groups.get(fact.area).push(fact);
|
|
486
|
+
}
|
|
487
|
+
const slices = Array.from(groups.entries()).map(([area, files]) => {
|
|
488
|
+
const sorted = files.slice().sort((a, b) => semanticFactScore(b) - semanticFactScore(a) || b.tokens - a.tokens || a.path.localeCompare(b.path));
|
|
489
|
+
const included = sorted.slice(0, SEMANTIC_SLICE_LIMIT_PER_AREA);
|
|
490
|
+
const sourceTokens = files.reduce((acc, item) => acc + Number(item.tokens || 0), 0);
|
|
491
|
+
const factCount = files.reduce((acc, item) => acc + semanticFactCount(item), 0);
|
|
492
|
+
const sliceSeed = {
|
|
493
|
+
area,
|
|
494
|
+
files: files.map((item) => ({ path: item.path, hash: item.hash, tokens: item.tokens })).sort((a, b) => a.path.localeCompare(b.path))
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
id: `sem_${sha256(stableJson(sliceSeed)).slice(0, 12)}`,
|
|
498
|
+
area,
|
|
499
|
+
title: semanticAreaTitle(area),
|
|
500
|
+
file_count: files.length,
|
|
501
|
+
included_file_count: included.length,
|
|
502
|
+
omitted_file_count: Math.max(0, files.length - included.length),
|
|
503
|
+
source_tokens_covered: sourceTokens,
|
|
504
|
+
fact_count: factCount,
|
|
505
|
+
files: included
|
|
506
|
+
};
|
|
507
|
+
}).sort((a, b) => b.source_tokens_covered - a.source_tokens_covered || a.title.localeCompare(b.title));
|
|
508
|
+
const receiptSeed = {
|
|
509
|
+
artifact_version: 1,
|
|
510
|
+
mode: "offline-semantic-slices",
|
|
511
|
+
repo: path.basename(repo),
|
|
512
|
+
files_indexed: summary.files.length,
|
|
513
|
+
source_tokens_indexed: summary.totalTokens,
|
|
514
|
+
slices: slices.map((slice) => ({
|
|
515
|
+
id: slice.id,
|
|
516
|
+
area: slice.area,
|
|
517
|
+
file_count: slice.file_count,
|
|
518
|
+
source_tokens_covered: slice.source_tokens_covered,
|
|
519
|
+
fact_count: slice.fact_count
|
|
520
|
+
}))
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
artifact_version: 1,
|
|
524
|
+
mode: "offline-semantic-slices",
|
|
525
|
+
created_utc: new Date().toISOString(),
|
|
526
|
+
repo: path.basename(repo),
|
|
527
|
+
files_indexed: summary.files.length,
|
|
528
|
+
source_tokens_indexed: summary.totalTokens,
|
|
529
|
+
file_fact_count: facts.length,
|
|
530
|
+
fact_count: slices.reduce((acc, slice) => acc + slice.fact_count, 0),
|
|
531
|
+
receipt_sha256: sha256(stableJson(receiptSeed)),
|
|
532
|
+
slices
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function buildSemanticSlicesMarkdown(index) {
|
|
537
|
+
const lines = [];
|
|
538
|
+
lines.push("# CostLayers Semantic Slices");
|
|
539
|
+
lines.push("");
|
|
540
|
+
lines.push("Offline semantic facts from local source scanning. This artifact is not used for live gateway request reduction; use it to choose exact files before opening broad context.");
|
|
541
|
+
lines.push("");
|
|
542
|
+
lines.push(`Receipt: ${index.receipt_sha256}`);
|
|
543
|
+
lines.push(`Files indexed: ${index.files_indexed.toLocaleString()}`);
|
|
544
|
+
lines.push(`Source tokens covered: ${index.source_tokens_indexed.toLocaleString()}`);
|
|
545
|
+
lines.push(`Slices: ${index.slices.length.toLocaleString()}`);
|
|
546
|
+
lines.push("");
|
|
547
|
+
for (const slice of index.slices) {
|
|
548
|
+
lines.push(`## ${slice.title}`);
|
|
549
|
+
lines.push(`- Slice id: ${slice.id}`);
|
|
550
|
+
lines.push(`- Files covered: ${slice.file_count.toLocaleString()}`);
|
|
551
|
+
lines.push(`- Source tokens covered: ${slice.source_tokens_covered.toLocaleString()}`);
|
|
552
|
+
if (slice.omitted_file_count > 0) lines.push(`- Additional files in JSON artifact: ${slice.omitted_file_count.toLocaleString()}`);
|
|
553
|
+
for (const file of slice.files.slice(0, 12)) {
|
|
554
|
+
lines.push(`- ${file.path} (${Number(file.tokens || 0).toLocaleString()} tokens, ${Number(file.lines || 0).toLocaleString()} lines, hash ${file.hash})`);
|
|
555
|
+
if (file.routes.length) lines.push(` - routes: ${file.routes.slice(0, 3).join(" | ")}`);
|
|
556
|
+
if (file.symbols.length) lines.push(` - symbols: ${file.symbols.slice(0, 8).join(", ")}`);
|
|
557
|
+
if (file.imports.length) lines.push(` - imports: ${file.imports.slice(0, 3).join(" | ")}`);
|
|
558
|
+
}
|
|
559
|
+
lines.push("");
|
|
560
|
+
}
|
|
561
|
+
return lines.join("\n");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function semanticReportSummary(index, semanticMarkdown, baselineBroadReadTokens) {
|
|
565
|
+
const semanticIndexTokens = estimateTokens(semanticMarkdown);
|
|
566
|
+
const potentialAvoided = Math.max(0, Number(baselineBroadReadTokens || 0) - semanticIndexTokens);
|
|
567
|
+
const potentialPct = baselineBroadReadTokens > 0 ? potentialAvoided / baselineBroadReadTokens * 100 : 0;
|
|
568
|
+
return {
|
|
569
|
+
mode: "offline_local_only",
|
|
570
|
+
artifact: ".agentspend/semantic-slices.md",
|
|
571
|
+
json_artifact: ".agentspend/semantic-slices.json",
|
|
572
|
+
receipt_hash: index.receipt_sha256,
|
|
573
|
+
slice_count: index.slices.length,
|
|
574
|
+
file_fact_count: index.file_fact_count,
|
|
575
|
+
fact_count: index.fact_count,
|
|
576
|
+
source_tokens_covered: index.source_tokens_indexed,
|
|
577
|
+
semantic_index_tokens: semanticIndexTokens,
|
|
578
|
+
potential_tokens_avoided_per_repeated_task: potentialAvoided,
|
|
579
|
+
potential_reduction_percent: Number(potentialPct.toFixed(2)),
|
|
580
|
+
top_slices: index.slices.slice(0, 8).map((slice) => ({
|
|
581
|
+
id: slice.id,
|
|
582
|
+
title: slice.title,
|
|
583
|
+
file_count: slice.file_count,
|
|
584
|
+
source_tokens_covered: slice.source_tokens_covered,
|
|
585
|
+
fact_count: slice.fact_count
|
|
586
|
+
}))
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
393
590
|
function buildInstructions() {
|
|
394
591
|
return `# CostLayers Agent Instructions
|
|
395
592
|
|
|
396
593
|
Before broad repo exploration:
|
|
397
594
|
|
|
398
595
|
1. Read .agentspend/repo-pack.md.
|
|
399
|
-
2.
|
|
400
|
-
3.
|
|
401
|
-
4.
|
|
596
|
+
2. Read .agentspend/semantic-slices.md for route and symbol facts when a compact semantic map is enough.
|
|
597
|
+
3. Use the listed entry points, route hints, and symbol hints to target file reads.
|
|
598
|
+
4. Avoid rereading unchanged large files unless exact code is required.
|
|
599
|
+
5. After major repo changes, ask the user to run \`costlayers scan\`.
|
|
402
600
|
|
|
403
601
|
Goal: reduce repeated context spend while preserving answer quality.
|
|
404
602
|
`;
|
|
@@ -412,15 +610,16 @@ This repo uses CostLayers to reduce repeated AI coding-agent context spend.
|
|
|
412
610
|
Before broad exploration:
|
|
413
611
|
|
|
414
612
|
1. Read .agentspend/repo-pack.md if it exists.
|
|
415
|
-
2. Read .agentspend/
|
|
416
|
-
3.
|
|
417
|
-
4.
|
|
418
|
-
5.
|
|
613
|
+
2. Read .agentspend/semantic-slices.md when route and symbol facts are enough to target the next read.
|
|
614
|
+
3. Read .agentspend/agent-instructions.md.
|
|
615
|
+
4. Prefer targeted file reads based on the repo pack.
|
|
616
|
+
5. Avoid rereading unchanged large files unless exact code is required.
|
|
617
|
+
6. After major repo changes, run or ask for \`costlayers scan\`.
|
|
419
618
|
|
|
420
619
|
`;
|
|
421
620
|
}
|
|
422
621
|
|
|
423
|
-
function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek) {
|
|
622
|
+
function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek, semanticIndex = null, semanticMarkdown = "") {
|
|
424
623
|
const packTokens = estimateTokens(repoPack);
|
|
425
624
|
const broadReadTokens = Math.max(
|
|
426
625
|
summary.totalTokens,
|
|
@@ -434,6 +633,7 @@ function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek) {
|
|
|
434
633
|
const projectedWeeklyUsd = savedPerRunUsd * runsPerWeek;
|
|
435
634
|
const projectedMonthlyUsd = projectedWeeklyUsd * WEEKS_PER_MONTH;
|
|
436
635
|
const reductionPct = broadReadTokens > 0 ? avoidedPerTask / broadReadTokens * 100 : 0;
|
|
636
|
+
const semanticSummary = semanticIndex ? semanticReportSummary(semanticIndex, semanticMarkdown, broadReadTokens) : null;
|
|
437
637
|
return {
|
|
438
638
|
created_utc: new Date().toISOString(),
|
|
439
639
|
files_indexed: summary.files.length,
|
|
@@ -453,24 +653,99 @@ function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek) {
|
|
|
453
653
|
path: row.rel,
|
|
454
654
|
tokens: row.tokens,
|
|
455
655
|
hash: row.hash
|
|
456
|
-
}))
|
|
656
|
+
})),
|
|
657
|
+
...(semanticSummary ? { semantic_slices: semanticSummary } : {})
|
|
457
658
|
};
|
|
458
659
|
}
|
|
459
660
|
|
|
661
|
+
function summaryFingerprint(summary) {
|
|
662
|
+
const files = (summary.files || []).map((row) => ({
|
|
663
|
+
path: row.rel,
|
|
664
|
+
size: row.size,
|
|
665
|
+
hash: row.hash,
|
|
666
|
+
tokens: row.tokens
|
|
667
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
668
|
+
return sha256(stableJson({ artifact_version: 1, files }));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function readLocalCache(outDir) {
|
|
672
|
+
return readJsonIfExists(path.join(outDir, "local-cache.json"));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function writeLocalCache(outDir, payload) {
|
|
676
|
+
fs.writeFileSync(path.join(outDir, "local-cache.json"), JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
677
|
+
}
|
|
678
|
+
|
|
460
679
|
function scanToFiles(repo, args) {
|
|
461
680
|
const outDir = path.join(repo, ".agentspend");
|
|
462
681
|
ensureDir(outDir);
|
|
682
|
+
ensureCostLayersGitignore(outDir);
|
|
463
683
|
const tasks = Number(args.tasks || 100);
|
|
464
684
|
const runsPerWeek = Number(args["runs-per-week"] || DEFAULT_RUNS_PER_WEEK);
|
|
465
685
|
const pricePer1m = Number(args["price-per-1m"] || 2.0);
|
|
466
686
|
const summary = summarizeRepo(repo);
|
|
687
|
+
const fingerprint = summaryFingerprint(summary);
|
|
688
|
+
const cached = readLocalCache(outDir);
|
|
689
|
+
if (!args.force && cached && cached.fingerprint === fingerprint) {
|
|
690
|
+
const packFile = path.join(outDir, "repo-pack.md");
|
|
691
|
+
const semanticJsonFile = path.join(outDir, "semantic-slices.json");
|
|
692
|
+
const semanticMdFile = path.join(outDir, "semantic-slices.md");
|
|
693
|
+
const reportFile = path.join(outDir, "savings-report.json");
|
|
694
|
+
if (fs.existsSync(packFile) && fs.existsSync(semanticJsonFile) && fs.existsSync(semanticMdFile) && fs.existsSync(reportFile)) {
|
|
695
|
+
return {
|
|
696
|
+
outDir,
|
|
697
|
+
summary,
|
|
698
|
+
pack: fs.readFileSync(packFile, "utf8"),
|
|
699
|
+
report: readJsonIfExists(reportFile),
|
|
700
|
+
semanticIndex: readJsonIfExists(semanticJsonFile),
|
|
701
|
+
semanticMarkdown: fs.readFileSync(semanticMdFile, "utf8"),
|
|
702
|
+
localCacheHit: true,
|
|
703
|
+
fingerprint
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const semanticIndex = buildSemanticSlices(repo, summary);
|
|
708
|
+
const semanticMarkdown = buildSemanticSlicesMarkdown(semanticIndex);
|
|
467
709
|
const pack = buildRepoPack(repo, summary);
|
|
468
|
-
const report = buildReport(summary, pack, tasks, pricePer1m, runsPerWeek);
|
|
710
|
+
const report = buildReport(summary, pack, tasks, pricePer1m, runsPerWeek, semanticIndex, semanticMarkdown);
|
|
469
711
|
fs.writeFileSync(path.join(outDir, "repo-pack.md"), pack, "utf8");
|
|
712
|
+
fs.writeFileSync(path.join(outDir, "semantic-slices.json"), JSON.stringify(semanticIndex, null, 2) + "\n", "utf8");
|
|
713
|
+
fs.writeFileSync(path.join(outDir, "semantic-slices.md"), semanticMarkdown, "utf8");
|
|
470
714
|
fs.writeFileSync(path.join(outDir, "agent-instructions.md"), buildInstructions(), "utf8");
|
|
471
715
|
fs.writeFileSync(path.join(outDir, "savings-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
472
716
|
fs.writeFileSync(path.join(outDir, "savings-report.md"), reportMarkdown(report), "utf8");
|
|
473
|
-
|
|
717
|
+
writeLocalCache(outDir, {
|
|
718
|
+
artifact_version: 1,
|
|
719
|
+
fingerprint,
|
|
720
|
+
created_utc: new Date().toISOString(),
|
|
721
|
+
files_indexed: summary.files.length,
|
|
722
|
+
source_tokens_indexed: summary.totalTokens,
|
|
723
|
+
repo_pack_sha256: sha256(pack),
|
|
724
|
+
report_sha256: sha256(stableJson(report)),
|
|
725
|
+
semantic_receipt_sha256: semanticIndex.receipt_sha256
|
|
726
|
+
});
|
|
727
|
+
return { outDir, summary, pack, report, semanticIndex, semanticMarkdown, localCacheHit: false, fingerprint };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
function semanticReportMarkdown(report) {
|
|
732
|
+
const semantic = report.semantic_slices;
|
|
733
|
+
if (!semantic || !semantic.slice_count) return "";
|
|
734
|
+
const top = Array.isArray(semantic.top_slices) ? semantic.top_slices : [];
|
|
735
|
+
return `## Offline Semantic Slices
|
|
736
|
+
|
|
737
|
+
- Artifact: \`${semantic.artifact}\`
|
|
738
|
+
- JSON: \`${semantic.json_artifact}\`
|
|
739
|
+
- Receipt hash: \`${semantic.receipt_hash}\`
|
|
740
|
+
- Slice count: ${Number(semantic.slice_count || 0).toLocaleString()}
|
|
741
|
+
- Semantic index tokens: ${Number(semantic.semantic_index_tokens || 0).toLocaleString()}
|
|
742
|
+
- Potential tokens avoided per repeated task: ${Number(semantic.potential_tokens_avoided_per_repeated_task || 0).toLocaleString()}
|
|
743
|
+
- Potential reduction vs broad read: ${semantic.potential_reduction_percent}%
|
|
744
|
+
|
|
745
|
+
${top.map((slice) => `- ${slice.title}: ${Number(slice.file_count || 0).toLocaleString()} files, ${Number(slice.source_tokens_covered || 0).toLocaleString()} source tokens, ${Number(slice.fact_count || 0).toLocaleString()} facts`).join("\n")}
|
|
746
|
+
|
|
747
|
+
These slices are generated offline from local file paths, hashes, imports, routes, and symbol lines. They enrich the local receipt and runtime plan without changing live provider prompts.
|
|
748
|
+
`;
|
|
474
749
|
}
|
|
475
750
|
|
|
476
751
|
function reportMarkdown(report) {
|
|
@@ -504,7 +779,7 @@ Generated: ${report.created_utc}
|
|
|
504
779
|
|
|
505
780
|
${report.largest_files.map((row) => `- ${row.path}: ${row.tokens.toLocaleString()} tokens, hash ${row.hash}`).join("\n")}
|
|
506
781
|
|
|
507
|
-
## Caveat
|
|
782
|
+
${semanticReportMarkdown(report)}## Caveat
|
|
508
783
|
|
|
509
784
|
This public scanner estimates repeated context waste. Real savings should be validated against provider usage or invoices.
|
|
510
785
|
`;
|
|
@@ -513,7 +788,7 @@ This public scanner estimates repeated context waste. Real savings should be val
|
|
|
513
788
|
function init(repo, options = {}) {
|
|
514
789
|
const outDir = path.join(repo, ".agentspend");
|
|
515
790
|
ensureDir(outDir);
|
|
516
|
-
|
|
791
|
+
ensureCostLayersGitignore(outDir);
|
|
517
792
|
writeIfMissing(path.join(outDir, "config.json"), JSON.stringify({
|
|
518
793
|
version: VERSION,
|
|
519
794
|
created_utc: new Date().toISOString(),
|
|
@@ -532,14 +807,21 @@ function init(repo, options = {}) {
|
|
|
532
807
|
if (!options.suppressNext) process.stdout.write("Next: costlayers scan\n");
|
|
533
808
|
}
|
|
534
809
|
|
|
535
|
-
function scan(repo, args) {
|
|
810
|
+
async function scan(repo, args) {
|
|
536
811
|
process.stdout.write(`Scanning repo: ${repo}\n`);
|
|
537
812
|
const precomputed = scanToFiles(repo, args);
|
|
538
813
|
const { outDir, report } = precomputed;
|
|
539
814
|
process.stdout.write(`CostLayers scan complete\n`);
|
|
815
|
+
if (precomputed.localCacheHit) process.stdout.write(`Local exact cache hit: source hashes unchanged, reused CostLayers artifacts\n`);
|
|
540
816
|
process.stdout.write(`Repo pack: ${path.join(outDir, "repo-pack.md")}\n`);
|
|
817
|
+
process.stdout.write(`Semantic slices: ${path.join(outDir, "semantic-slices.md")}\n`);
|
|
541
818
|
process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
|
|
542
819
|
printSavingsSummary(report);
|
|
820
|
+
await trackEvent(repo, args, "cli_scan", {
|
|
821
|
+
files_indexed: report.files_indexed,
|
|
822
|
+
context_tokens_avoided_per_task: report.tokens_avoided_per_repeated_task,
|
|
823
|
+
local_cache_hit: Boolean(precomputed.localCacheHit)
|
|
824
|
+
});
|
|
543
825
|
}
|
|
544
826
|
|
|
545
827
|
function connectEngine(repo, args) {
|
|
@@ -582,6 +864,58 @@ function loadConnection(repo, args) {
|
|
|
582
864
|
};
|
|
583
865
|
}
|
|
584
866
|
|
|
867
|
+
function localAudit(repo) {
|
|
868
|
+
const auditPath = path.join(repo, ".agentspend", "ai-spend-audit.json");
|
|
869
|
+
const savingsPath = path.join(repo, ".agentspend", "savings-report.json");
|
|
870
|
+
const auditReportPath = path.join(repo, ".agentspend", "ai-spend-audit.md");
|
|
871
|
+
const savingsReportPath = path.join(repo, ".agentspend", "savings-report.md");
|
|
872
|
+
const audit = readJsonIfExists(auditPath);
|
|
873
|
+
const savings = readJsonIfExists(savingsPath);
|
|
874
|
+
if (!audit && !savings) return null;
|
|
875
|
+
return { audit, savings, auditReportPath, savingsReportPath };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function printLocalDashboard(repo) {
|
|
879
|
+
const local = localAudit(repo);
|
|
880
|
+
if (!local) {
|
|
881
|
+
process.stderr.write([
|
|
882
|
+
"No CostLayers dashboard is available for this repo yet.",
|
|
883
|
+
"",
|
|
884
|
+
"Run a free local audit first:",
|
|
885
|
+
` npx -y ${INSTALL_SPEC} audit --monthly-spend 10000`,
|
|
886
|
+
"",
|
|
887
|
+
"Or create a hosted dashboard:",
|
|
888
|
+
` npx -y ${INSTALL_SPEC} signup --email you@example.com`,
|
|
889
|
+
""
|
|
890
|
+
].join("\n"));
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
const audit = local.audit || {};
|
|
894
|
+
const savings = local.savings || {};
|
|
895
|
+
const contextTokens = audit.tokens_avoided_per_run ?? savings.tokens_avoided_per_repeated_task ?? 0;
|
|
896
|
+
const contextReduction = audit.context_reduction_percent ?? savings.estimated_reduction_percent ?? 0;
|
|
897
|
+
const contextPackTokens = audit.context_pack_tokens ?? savings.context_pack_tokens ?? 0;
|
|
898
|
+
const sourceTokens = audit.source_tokens_indexed ?? savings.source_tokens_indexed ?? 0;
|
|
899
|
+
process.stdout.write(`CostLayers Local Dashboard\n`);
|
|
900
|
+
process.stdout.write(`status: local audit only; no hosted account connected\n`);
|
|
901
|
+
process.stdout.write(`repo: ${path.basename(repo)}\n`);
|
|
902
|
+
if (audit.spend_analyzed_display) process.stdout.write(`ai_spend_analyzed: ${audit.spend_analyzed_display}\n`);
|
|
903
|
+
if (audit.waste_found_display) process.stdout.write(`candidate_waste_found: ${audit.waste_found_display}\n`);
|
|
904
|
+
if (audit.safe_savings_display) process.stdout.write(`quality_safe_savings_now: ${audit.safe_savings_display}\n`);
|
|
905
|
+
process.stdout.write(`context_tokens_avoided_per_agent_run: ${formatInt(contextTokens)}\n`);
|
|
906
|
+
process.stdout.write(`context_reduction: ${Number(contextReduction || 0).toFixed(2)}%\n`);
|
|
907
|
+
process.stdout.write(`before_after_context: ${formatInt(sourceTokens)} -> ${formatInt(contextPackTokens)} tokens\n`);
|
|
908
|
+
process.stdout.write(`local_report: ${local.audit ? local.auditReportPath : local.savingsReportPath}\n`);
|
|
909
|
+
process.stdout.write(`\nNext:\n`);
|
|
910
|
+
process.stdout.write(` Open the local report above, or create the hosted savings meter:\n`);
|
|
911
|
+
process.stdout.write(` npx -y ${INSTALL_SPEC} signup --email you@example.com\n`);
|
|
912
|
+
process.stdout.write(`\nEvidence labels:\n`);
|
|
913
|
+
process.stdout.write(` Context estimate: local scan, visible immediately.\n`);
|
|
914
|
+
process.stdout.write(` Usage stretch: appears after Codex profile token events.\n`);
|
|
915
|
+
process.stdout.write(` API invoice savings: appears only after API-billed traffic is routed through CostLayers.\n`);
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
|
|
585
919
|
function defaultPublicGatewayUrl(engineUrl, apiKey) {
|
|
586
920
|
try {
|
|
587
921
|
const url = new URL(engineUrl);
|
|
@@ -620,14 +954,45 @@ function savingsProjection(report) {
|
|
|
620
954
|
};
|
|
621
955
|
}
|
|
622
956
|
|
|
957
|
+
function savingsVerdict(report) {
|
|
958
|
+
const projection = savingsProjection(report);
|
|
959
|
+
const avoided = Number(report.tokens_avoided_per_repeated_task || 0);
|
|
960
|
+
const sourceTokens = Number(report.source_tokens_indexed || 0);
|
|
961
|
+
const reduction = Number(report.estimated_reduction_percent || 0);
|
|
962
|
+
const meaningful = avoided >= 1000 && reduction >= 15 && projection.monthlyUsd >= 0.25;
|
|
963
|
+
return {
|
|
964
|
+
projection,
|
|
965
|
+
avoided,
|
|
966
|
+
sourceTokens,
|
|
967
|
+
reduction,
|
|
968
|
+
meaningful,
|
|
969
|
+
smallRepo: sourceTokens < 5000 || avoided < 1000
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
623
973
|
function dashboardUrlFromConnection(connection) {
|
|
624
974
|
return (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
|
|
625
975
|
}
|
|
626
976
|
|
|
627
|
-
function printSavingsSummary(report) {
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
|
|
977
|
+
function printSavingsSummary(report, options = {}) {
|
|
978
|
+
const verdict = savingsVerdict(report);
|
|
979
|
+
const projection = verdict.projection;
|
|
980
|
+
const avoided = verdict.avoided;
|
|
981
|
+
if (options.compact) {
|
|
982
|
+
if (!verdict.meaningful) {
|
|
983
|
+
process.stdout.write(`CostLayers: repo scanned; no meaningful savings claim for this small/simple repo.\n`);
|
|
984
|
+
} else {
|
|
985
|
+
process.stdout.write(`CostLayers found context waste: ${formatInt(report.tokens_avoided_per_repeated_task)} tokens/run avoided (${report.estimated_reduction_percent}% less).\n`);
|
|
986
|
+
process.stdout.write(`Usage-stretch estimate: ${formatUsd(projection.monthlyUsd)}/month at ${formatInt(projection.runsPerWeek)} agent runs/week.\n`);
|
|
987
|
+
}
|
|
988
|
+
process.stdout.write(`Invoice savings require API mode; ChatGPT-login mode keeps native Codex auth.\n`);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (!verdict.meaningful) {
|
|
992
|
+
process.stdout.write(`\nCostLayers installed. This repo is too small for a meaningful savings claim.\n`);
|
|
993
|
+
} else {
|
|
994
|
+
process.stdout.write(`\nCostLayers found repeated context waste\n`);
|
|
995
|
+
}
|
|
631
996
|
process.stdout.write(` Evidence: context estimate from local repo scan\n`);
|
|
632
997
|
process.stdout.write(` Tokens avoided per repeated task: ${formatInt(report.tokens_avoided_per_repeated_task)}\n`);
|
|
633
998
|
if (avoided > 0) {
|
|
@@ -638,9 +1003,187 @@ function printSavingsSummary(report) {
|
|
|
638
1003
|
}
|
|
639
1004
|
process.stdout.write(` Estimated waste value per ${formatInt(report.repeated_tasks_modeled)} repeated tasks: ${formatUsd(report.estimated_usd_saved)}\n`);
|
|
640
1005
|
process.stdout.write(` Usage-stretch estimate at ${formatInt(projection.runsPerWeek)} agent runs/week: ${formatUsd(projection.weeklyUsd)}/week, ${formatUsd(projection.monthlyUsd)}/month\n`);
|
|
1006
|
+
if (!verdict.meaningful) {
|
|
1007
|
+
process.stdout.write(` Verdict: no savings claim for this repo. CostLayers is strongest on large repos and repeated agent sessions.\n`);
|
|
1008
|
+
}
|
|
641
1009
|
process.stdout.write(` Invoice savings require API traffic through CostLayers invoice mode.\n`);
|
|
642
1010
|
process.stdout.write(` Source tokens indexed: ${formatInt(report.source_tokens_indexed)}\n`);
|
|
643
1011
|
process.stdout.write(` Compact repo pack: ${formatInt(report.context_pack_tokens)} tokens\n`);
|
|
1012
|
+
const semantic = report.semantic_slices || null;
|
|
1013
|
+
if (semantic && semantic.slice_count) {
|
|
1014
|
+
process.stdout.write(` Offline semantic slices: ${formatInt(semantic.slice_count)} slices, ${formatInt(semantic.semantic_index_tokens)} tokens, receipt ${String(semantic.receipt_hash || "").slice(0, 12)}\n`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function moneyNumber(value) {
|
|
1019
|
+
const text = String(value || "").replace(/[$,_\s]/g, "");
|
|
1020
|
+
const number = Number(text);
|
|
1021
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function usageSpendFromObject(value) {
|
|
1025
|
+
if (!value) return 0;
|
|
1026
|
+
if (Array.isArray(value)) return value.reduce((sum, item) => sum + usageSpendFromObject(item), 0);
|
|
1027
|
+
if (typeof value !== "object") return 0;
|
|
1028
|
+
const directKeys = [
|
|
1029
|
+
"cost_usd",
|
|
1030
|
+
"total_cost_usd",
|
|
1031
|
+
"billed_usd",
|
|
1032
|
+
"spend_usd",
|
|
1033
|
+
"amount_usd",
|
|
1034
|
+
"total_usd",
|
|
1035
|
+
"cost",
|
|
1036
|
+
"total_cost",
|
|
1037
|
+
"amount",
|
|
1038
|
+
"usd"
|
|
1039
|
+
];
|
|
1040
|
+
for (const key of directKeys) {
|
|
1041
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
1042
|
+
const amount = moneyNumber(value[key]);
|
|
1043
|
+
if (amount > 0) return amount;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const nestedKeys = ["data", "rows", "items", "usage", "results", "records"];
|
|
1047
|
+
let total = 0;
|
|
1048
|
+
for (const key of nestedKeys) {
|
|
1049
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) total += usageSpendFromObject(value[key]);
|
|
1050
|
+
}
|
|
1051
|
+
return total;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function parseCsvSpend(text) {
|
|
1055
|
+
const lines = String(text || "").split(/\r?\n/).filter(Boolean);
|
|
1056
|
+
if (lines.length < 2) return 0;
|
|
1057
|
+
const headers = lines[0].split(",").map((item) => item.trim().replace(/^"|"$/g, "").toLowerCase());
|
|
1058
|
+
const spendColumns = headers
|
|
1059
|
+
.map((name, index) => ({ name, index }))
|
|
1060
|
+
.filter(({ name }) => /^(cost_usd|total_cost_usd|billed_usd|spend_usd|amount_usd|total_usd|cost|amount|usd)$/.test(name));
|
|
1061
|
+
if (!spendColumns.length) return 0;
|
|
1062
|
+
let total = 0;
|
|
1063
|
+
for (const line of lines.slice(1)) {
|
|
1064
|
+
const cells = line.split(",");
|
|
1065
|
+
const column = spendColumns.find(({ index }) => moneyNumber(cells[index]) > 0);
|
|
1066
|
+
if (column) total += moneyNumber(cells[column.index]);
|
|
1067
|
+
}
|
|
1068
|
+
return total;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function loadSpendInput(args) {
|
|
1072
|
+
const supplied = moneyNumber(args["monthly-spend"] || args.spend);
|
|
1073
|
+
if (supplied > 0) return { monthlySpendUsd: supplied, source: "user-supplied monthly spend" };
|
|
1074
|
+
const usageFile = args["usage-file"] ? path.resolve(String(args["usage-file"])) : "";
|
|
1075
|
+
if (!usageFile) return { monthlySpendUsd: 0, source: "not supplied" };
|
|
1076
|
+
if (!fs.existsSync(usageFile)) {
|
|
1077
|
+
process.stderr.write(`Usage file not found: ${usageFile}\n`);
|
|
1078
|
+
process.exit(2);
|
|
1079
|
+
}
|
|
1080
|
+
const text = fs.readFileSync(usageFile, "utf8");
|
|
1081
|
+
let total = 0;
|
|
1082
|
+
if (usageFile.toLowerCase().endsWith(".json")) {
|
|
1083
|
+
try {
|
|
1084
|
+
total = usageSpendFromObject(JSON.parse(text));
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
process.stderr.write(`Could not parse usage JSON: ${err.message}\n`);
|
|
1087
|
+
process.exit(2);
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
total = parseCsvSpend(text);
|
|
1091
|
+
}
|
|
1092
|
+
return { monthlySpendUsd: total, source: `usage import: ${usageFile}` };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function buildAuditMarkdown(audit) {
|
|
1096
|
+
return `# CostLayers AI Spend Audit
|
|
1097
|
+
|
|
1098
|
+
Generated: ${audit.created_utc}
|
|
1099
|
+
Repo: ${audit.repo}
|
|
1100
|
+
|
|
1101
|
+
## Executive Summary
|
|
1102
|
+
|
|
1103
|
+
- AI spend analyzed: ${audit.spend_analyzed_display}
|
|
1104
|
+
- Candidate waste found: ${audit.waste_found_display}
|
|
1105
|
+
- Quality-safe savings available now: ${audit.safe_savings_display}
|
|
1106
|
+
- First-scan context reduction: ${audit.context_reduction_percent}%
|
|
1107
|
+
- Repeated-context tokens avoided/run: ${audit.tokens_avoided_per_run.toLocaleString()}
|
|
1108
|
+
|
|
1109
|
+
## Evidence Labels
|
|
1110
|
+
|
|
1111
|
+
- Safe estimate: local repo scan found repeated context that agents can avoid before broad exploration.
|
|
1112
|
+
- Verified invoice savings: requires API-mode traffic through CostLayers and provider cost rows.
|
|
1113
|
+
- Risky savings: not counted here. CostLayers should forward unchanged when a reduction boundary is unsafe.
|
|
1114
|
+
|
|
1115
|
+
## Recommended Next Step
|
|
1116
|
+
|
|
1117
|
+
${audit.next_step}
|
|
1118
|
+
|
|
1119
|
+
## Largest Repeated Context Sources
|
|
1120
|
+
|
|
1121
|
+
${audit.largest_files.map((row) => `- ${row.path}: ${row.tokens.toLocaleString()} tokens`).join("\n")}
|
|
1122
|
+
`;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function buildAuditReport(repo, report, args) {
|
|
1126
|
+
const projection = savingsProjection(report);
|
|
1127
|
+
const spendInput = loadSpendInput(args);
|
|
1128
|
+
const spendAnalyzed = spendInput.monthlySpendUsd;
|
|
1129
|
+
const contextSafeSavings = projection.monthlyUsd;
|
|
1130
|
+
const contextRatio = Math.max(0, Math.min(1, Number(report.estimated_reduction_percent || 0) / 100));
|
|
1131
|
+
const budgetWasteEstimate = spendAnalyzed > 0 ? spendAnalyzed * Math.min(0.25, contextRatio * 0.2) : 0;
|
|
1132
|
+
const wasteFound = Math.max(contextSafeSavings, budgetWasteEstimate);
|
|
1133
|
+
const safeSavings = contextSafeSavings;
|
|
1134
|
+
return {
|
|
1135
|
+
created_utc: new Date().toISOString(),
|
|
1136
|
+
repo: path.basename(repo),
|
|
1137
|
+
spend_source: spendInput.source,
|
|
1138
|
+
monthly_spend_analyzed_usd: Number(spendAnalyzed.toFixed(6)),
|
|
1139
|
+
waste_found_usd_per_month: Number(wasteFound.toFixed(6)),
|
|
1140
|
+
quality_safe_savings_usd_per_month: Number(safeSavings.toFixed(6)),
|
|
1141
|
+
context_reduction_percent: Number(report.estimated_reduction_percent || 0),
|
|
1142
|
+
tokens_avoided_per_run: Number(report.tokens_avoided_per_repeated_task || 0),
|
|
1143
|
+
source_tokens_indexed: Number(report.source_tokens_indexed || 0),
|
|
1144
|
+
context_pack_tokens: Number(report.context_pack_tokens || 0),
|
|
1145
|
+
largest_files: report.largest_files || [],
|
|
1146
|
+
spend_analyzed_display: spendAnalyzed > 0 ? `${formatUsd(spendAnalyzed)}/month (${spendInput.source})` : "not supplied; using repo-scan savings only",
|
|
1147
|
+
waste_found_display: `${formatUsd(wasteFound)}/month candidate estimate`,
|
|
1148
|
+
safe_savings_display: `${formatUsd(safeSavings)}/month from local context scan`,
|
|
1149
|
+
next_step: spendAnalyzed > 0
|
|
1150
|
+
? "Enable API invoice mode on one controlled coding-agent workflow so CostLayers can turn the audit estimate into verified provider-dollar savings."
|
|
1151
|
+
: "Rerun with --monthly-spend <usd> or --usage-file <path>, then enable API invoice mode on one controlled workflow."
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function writeAuditReport(outDir, repo, report, args) {
|
|
1156
|
+
const auditReport = buildAuditReport(repo, report, args);
|
|
1157
|
+
fs.writeFileSync(path.join(outDir, "ai-spend-audit.json"), JSON.stringify(auditReport, null, 2) + "\n", "utf8");
|
|
1158
|
+
fs.writeFileSync(path.join(outDir, "ai-spend-audit.md"), buildAuditMarkdown(auditReport), "utf8");
|
|
1159
|
+
return auditReport;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
async function audit(repo, args) {
|
|
1163
|
+
process.stdout.write(`Running CostLayers AI spend audit for: ${repo}\n`);
|
|
1164
|
+
const precomputed = scanToFiles(repo, args);
|
|
1165
|
+
const { outDir, report } = precomputed;
|
|
1166
|
+
const auditReport = writeAuditReport(outDir, repo, report, args);
|
|
1167
|
+
process.stdout.write(`\nCostLayers AI Spend Audit\n`);
|
|
1168
|
+
process.stdout.write(`\nYou found repeated context waste\n`);
|
|
1169
|
+
process.stdout.write(` AI spend analyzed: ${auditReport.spend_analyzed_display}\n`);
|
|
1170
|
+
process.stdout.write(` Candidate waste found: ${auditReport.waste_found_display}\n`);
|
|
1171
|
+
process.stdout.write(` Quality-safe savings available now: ${auditReport.safe_savings_display}\n`);
|
|
1172
|
+
process.stdout.write(` Context reduction: ${auditReport.context_reduction_percent}%\n`);
|
|
1173
|
+
process.stdout.write(` Tokens avoided per agent run: ${formatInt(auditReport.tokens_avoided_per_run)}\n`);
|
|
1174
|
+
process.stdout.write(` Audit report: ${path.join(outDir, "ai-spend-audit.md")}\n`);
|
|
1175
|
+
process.stdout.write(`\nView this result any time:\n`);
|
|
1176
|
+
process.stdout.write(` npx -y ${INSTALL_SPEC} dashboard\n`);
|
|
1177
|
+
process.stdout.write(`\nNext best step:\n`);
|
|
1178
|
+
process.stdout.write(` ${auditReport.next_step}\n`);
|
|
1179
|
+
process.stdout.write(`\nHosted dashboard, optional:\n`);
|
|
1180
|
+
process.stdout.write(` npx -y ${INSTALL_SPEC} signup --email you@example.com\n`);
|
|
1181
|
+
await trackEvent(repo, args, "ai_spend_audit", {
|
|
1182
|
+
monthly_spend_analyzed_usd: auditReport.monthly_spend_analyzed_usd,
|
|
1183
|
+
waste_found_usd_per_month: auditReport.waste_found_usd_per_month,
|
|
1184
|
+
quality_safe_savings_usd_per_month: auditReport.quality_safe_savings_usd_per_month,
|
|
1185
|
+
context_tokens_avoided_per_task: auditReport.tokens_avoided_per_run
|
|
1186
|
+
});
|
|
644
1187
|
}
|
|
645
1188
|
|
|
646
1189
|
function codexHomeDir() {
|
|
@@ -698,6 +1241,15 @@ function profileTomlString(connection, args = {}) {
|
|
|
698
1241
|
const baseUrl = `${gateway}/v1`;
|
|
699
1242
|
const engineUrl = String(connection.engine_url || "https://costlayers.com/engine").replace(/\/+$/, "");
|
|
700
1243
|
const apiKeyEnv = codexProxyApiKeyEnv(args);
|
|
1244
|
+
const otelExporter = {
|
|
1245
|
+
"otlp-http": {
|
|
1246
|
+
endpoint: `${engineUrl}/v1/codex-meter`,
|
|
1247
|
+
protocol: "json",
|
|
1248
|
+
headers: {
|
|
1249
|
+
"x-costlayers-key": connection.api_key || ""
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
701
1253
|
const lines = [
|
|
702
1254
|
"# Generated by CostLayers. This profile sends Codex telemetry to the CostLayers meter.",
|
|
703
1255
|
"# Keep this file private because it contains your keyed CostLayers endpoint.",
|
|
@@ -723,18 +1275,27 @@ function profileTomlString(connection, args = {}) {
|
|
|
723
1275
|
"[otel]",
|
|
724
1276
|
'environment = "costlayers"',
|
|
725
1277
|
"log_user_prompt = false",
|
|
726
|
-
|
|
727
|
-
'[otel.exporter."otlp-http"]',
|
|
728
|
-
`endpoint = ${JSON.stringify(`${engineUrl}/v1/codex-meter`)}`,
|
|
729
|
-
'protocol = "json"',
|
|
730
|
-
"",
|
|
731
|
-
'[otel.exporter."otlp-http".headers]',
|
|
732
|
-
`"x-costlayers-key" = ${JSON.stringify(connection.api_key || "")}`,
|
|
1278
|
+
`exporter = ${toTomlInline(otelExporter)}`,
|
|
733
1279
|
""
|
|
734
1280
|
);
|
|
735
1281
|
return lines.join("\n");
|
|
736
1282
|
}
|
|
737
1283
|
|
|
1284
|
+
function toTomlInline(value) {
|
|
1285
|
+
if (value === null || value === undefined) return '""';
|
|
1286
|
+
if (Array.isArray(value)) return `[${value.map((item) => toTomlInline(item)).join(", ")}]`;
|
|
1287
|
+
if (typeof value === "object") {
|
|
1288
|
+
return `{ ${Object.entries(value).map(([key, item]) => `${tomlKey(key)} = ${toTomlInline(item)}`).join(", ")} }`;
|
|
1289
|
+
}
|
|
1290
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1291
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1292
|
+
return JSON.stringify(String(value));
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function tomlKey(key) {
|
|
1296
|
+
return /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
738
1299
|
function writeCodexProfile(connection, args = {}) {
|
|
739
1300
|
const dir = codexHomeDir();
|
|
740
1301
|
ensureDir(dir);
|
|
@@ -821,18 +1382,23 @@ async function ensureConnection(repo, args) {
|
|
|
821
1382
|
|
|
822
1383
|
async function signup(repo, args) {
|
|
823
1384
|
const connection = await signupConnection(repo, args);
|
|
1385
|
+
await trackEvent(repo, args, "signup", { label: connection.label || path.basename(repo) }, connection);
|
|
824
1386
|
process.stdout.write(`CostLayers self-serve key created\n`);
|
|
825
1387
|
process.stdout.write(`Engine: ${connection.engine_url}\n`);
|
|
826
1388
|
process.stdout.write(`Gateway: ${connection.gateway_url}\n`);
|
|
827
1389
|
process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
828
1390
|
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
829
1391
|
process.stdout.write(`Plan: free beta\n`);
|
|
830
|
-
process.stdout.write(
|
|
1392
|
+
process.stdout.write(`\nNext:\n`);
|
|
1393
|
+
process.stdout.write(` View dashboard: npx -y ${INSTALL_SPEC} dashboard\n`);
|
|
1394
|
+
process.stdout.write(` Run Codex with usage-stretch metering: npx -y ${INSTALL_SPEC} codex --email ${connection.email || "you@example.com"} --chatgpt\n`);
|
|
1395
|
+
process.stdout.write(` Prove API invoice savings: export OPENAI_API_KEY=sk-proj-... && npx -y ${INSTALL_SPEC} test --email ${connection.email || "you@example.com"}\n`);
|
|
831
1396
|
}
|
|
832
1397
|
|
|
833
1398
|
async function codexProfile(repo, args) {
|
|
834
1399
|
const connection = await ensureConnection(repo, args);
|
|
835
1400
|
const profilePath = writeCodexProfile(connection, args);
|
|
1401
|
+
await trackEvent(repo, args, "codex_profile", { profile_mode: codexProxyEnabled(args) ? "api" : "chatgpt" }, connection);
|
|
836
1402
|
process.stdout.write(`CostLayers Codex profile installed\n`);
|
|
837
1403
|
process.stdout.write(`Profile: ${profilePath}\n`);
|
|
838
1404
|
process.stdout.write(`Mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode, native Codex provider preserved"}\n`);
|
|
@@ -883,38 +1449,91 @@ function postJson(urlString, payload, apiKey) {
|
|
|
883
1449
|
});
|
|
884
1450
|
}
|
|
885
1451
|
|
|
1452
|
+
function engineUrlFromArgsOrConnection(args = {}, connection = null) {
|
|
1453
|
+
const fromArgs = String(args["engine-url"] || "").replace(/\/+$/, "");
|
|
1454
|
+
if (fromArgs) return fromArgs;
|
|
1455
|
+
const fromConnection = connection && connection.engine_url ? String(connection.engine_url).replace(/\/+$/, "") : "";
|
|
1456
|
+
return fromConnection || "https://costlayers.com/engine";
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
async function trackEvent(repo, args = {}, eventType, metadata = {}, connection = null) {
|
|
1460
|
+
if (args["no-telemetry"] || process.env.COSTLAYERS_DISABLE_TELEMETRY) return;
|
|
1461
|
+
const engineUrl = engineUrlFromArgsOrConnection(args, connection);
|
|
1462
|
+
const payload = {
|
|
1463
|
+
event_type: eventType,
|
|
1464
|
+
source: "costlayers-cli",
|
|
1465
|
+
email: normalizedEmail(args.email),
|
|
1466
|
+
metadata: {
|
|
1467
|
+
version: VERSION,
|
|
1468
|
+
repo_label: path.basename(repo || process.cwd()),
|
|
1469
|
+
command: args._ ? args._[0] : "",
|
|
1470
|
+
api_mode: apiInvoiceModeRequested(args),
|
|
1471
|
+
chatgpt_mode: chatgptModeRequested(args),
|
|
1472
|
+
platform: process.platform,
|
|
1473
|
+
node: process.version,
|
|
1474
|
+
...metadata
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
try {
|
|
1478
|
+
await postJson(`${engineUrl}/v1/event`, payload, connection ? connection.api_key : null);
|
|
1479
|
+
} catch {
|
|
1480
|
+
// Metrics must never block local developer workflow.
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
886
1484
|
function buildLocalPlan(report) {
|
|
1485
|
+
const semantic = report.semantic_slices || null;
|
|
1486
|
+
const instructions = [
|
|
1487
|
+
"Read .agentspend/repo-pack.md before broad exploration.",
|
|
1488
|
+
"Use .agentspend/savings-report.md to identify repeated context sources.",
|
|
1489
|
+
"Prefer targeted reads of files listed in the repo pack.",
|
|
1490
|
+
"Do not reread unchanged large files unless exact code is required."
|
|
1491
|
+
];
|
|
1492
|
+
if (semantic && semantic.slice_count) {
|
|
1493
|
+
instructions.splice(1, 0, "Read .agentspend/semantic-slices.md first when route or symbol facts are enough; open exact files only when needed.");
|
|
1494
|
+
}
|
|
887
1495
|
return {
|
|
888
1496
|
mode: "local",
|
|
889
1497
|
created_utc: new Date().toISOString(),
|
|
890
1498
|
plan_summary: "Use the repo pack before broad exploration and avoid rereading unchanged large files.",
|
|
891
1499
|
expected_value: {
|
|
892
1500
|
tokens_avoided_per_repeated_task: report.tokens_avoided_per_repeated_task,
|
|
893
|
-
estimated_usd_saved: report.estimated_usd_saved
|
|
1501
|
+
estimated_usd_saved: report.estimated_usd_saved,
|
|
1502
|
+
semantic_slice_count: semantic ? semantic.slice_count : 0,
|
|
1503
|
+
semantic_index_tokens: semantic ? semantic.semantic_index_tokens : 0,
|
|
1504
|
+
semantic_receipt_hash: semantic ? semantic.receipt_hash : ""
|
|
894
1505
|
},
|
|
895
|
-
runtime_instructions:
|
|
896
|
-
"Read .agentspend/repo-pack.md before broad exploration.",
|
|
897
|
-
"Use .agentspend/savings-report.md to identify repeated context sources.",
|
|
898
|
-
"Prefer targeted reads of files listed in the repo pack.",
|
|
899
|
-
"Do not reread unchanged large files unless exact code is required."
|
|
900
|
-
]
|
|
1506
|
+
runtime_instructions: instructions
|
|
901
1507
|
};
|
|
902
1508
|
}
|
|
903
1509
|
|
|
904
1510
|
async function fetchEnginePlan(connection, repo, pack, report) {
|
|
905
1511
|
if (!connection || !connection.engine_url) return null;
|
|
1512
|
+
const semantic = report && report.semantic_slices ? report.semantic_slices : {};
|
|
906
1513
|
const payload = {
|
|
907
1514
|
version: VERSION,
|
|
908
1515
|
repo_name: path.basename(repo),
|
|
909
1516
|
repo_pack_sha256: sha256(pack),
|
|
910
|
-
|
|
911
|
-
savings_report: report
|
|
1517
|
+
privacy_mode: argsPrivacyMode(connection),
|
|
1518
|
+
savings_report: report,
|
|
1519
|
+
local_artifacts: {
|
|
1520
|
+
repo_pack_sha256: sha256(pack),
|
|
1521
|
+
savings_report_sha256: sha256(stableJson(report || {})),
|
|
1522
|
+
semantic_receipt_hash: semantic && semantic.receipt_hash ? String(semantic.receipt_hash) : ""
|
|
1523
|
+
}
|
|
912
1524
|
};
|
|
1525
|
+
if (connection && connection.send_repo_preview) payload.repo_pack_preview = pack.slice(0, 12000);
|
|
913
1526
|
const plan = await postJson(`${connection.engine_url}/v1/plan`, payload, connection.api_key);
|
|
914
1527
|
plan.mode = plan.mode || "closed-engine";
|
|
915
1528
|
return plan;
|
|
916
1529
|
}
|
|
917
1530
|
|
|
1531
|
+
function argsPrivacyMode(connection) {
|
|
1532
|
+
return connection && connection.send_repo_preview
|
|
1533
|
+
? "opt_in_repo_preview"
|
|
1534
|
+
: "hashes_metrics_and_reports_default";
|
|
1535
|
+
}
|
|
1536
|
+
|
|
918
1537
|
function writeRuntimePrompt(outDir, plan) {
|
|
919
1538
|
const lines = [];
|
|
920
1539
|
lines.push("# CostLayers Runtime Plan");
|
|
@@ -946,27 +1565,33 @@ async function runAgent(repo, args, argv, options = {}) {
|
|
|
946
1565
|
if (connection && connection.engine_url) {
|
|
947
1566
|
try {
|
|
948
1567
|
plan = await fetchEnginePlan(connection, repo, pack, report);
|
|
949
|
-
process.stdout.write(`Fetched CostLayers engine plan\n`);
|
|
1568
|
+
if (!options.compactOutput) process.stdout.write(`Fetched CostLayers engine plan\n`);
|
|
950
1569
|
} catch (err) {
|
|
951
1570
|
process.stderr.write(`Engine unavailable; falling back to local plan: ${err.message}\n`);
|
|
952
1571
|
}
|
|
953
1572
|
}
|
|
954
1573
|
writeRuntimePrompt(outDir, plan);
|
|
955
|
-
process.stdout.write(`Runtime plan: ${path.join(outDir, "runtime-plan.md")}\n`);
|
|
1574
|
+
if (!options.compactOutput) process.stdout.write(`Runtime plan: ${path.join(outDir, "runtime-plan.md")}\n`);
|
|
956
1575
|
let commandToRun = command;
|
|
957
1576
|
if (connection && connection.engine_url && isCodexCommand(command)) {
|
|
958
1577
|
assertCodexProxyApiKey(args);
|
|
959
1578
|
const profilePath = writeCodexProfile(connection, args);
|
|
960
1579
|
commandToRun = withCostLayersCodexProfile(command);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1580
|
+
await trackEvent(repo, args, "codex_run", { profile_mode: codexProxyEnabled(args) ? "api" : "chatgpt" }, connection);
|
|
1581
|
+
if (options.compactOutput) {
|
|
1582
|
+
process.stdout.write(`Launching Codex with CostLayers profile: ${commandToRun.join(" ")}\n`);
|
|
1583
|
+
} else {
|
|
1584
|
+
process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
|
|
1585
|
+
process.stdout.write(`Codex metering enabled: ${commandToRun.join(" ")}\n`);
|
|
1586
|
+
process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
|
|
1587
|
+
process.stdout.write(`Savings dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
1588
|
+
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
1589
|
+
}
|
|
966
1590
|
}
|
|
967
1591
|
const env = {
|
|
968
1592
|
...process.env,
|
|
969
1593
|
AGENTSPEND_REPO_PACK: path.join(outDir, "repo-pack.md"),
|
|
1594
|
+
AGENTSPEND_SEMANTIC_SLICES: path.join(outDir, "semantic-slices.md"),
|
|
970
1595
|
AGENTSPEND_RUNTIME_PLAN: path.join(outDir, "runtime-plan.md")
|
|
971
1596
|
};
|
|
972
1597
|
const result = spawnSync(commandToRun[0], commandToRun.slice(1), {
|
|
@@ -1001,6 +1626,7 @@ async function gateway(repo, args) {
|
|
|
1001
1626
|
process.stdout.write(`CostLayers gateway ready: ${result.base_url}\n`);
|
|
1002
1627
|
process.stdout.write(`Set your OpenAI-compatible base URL to: ${result.base_url}\n`);
|
|
1003
1628
|
process.stdout.write(`Report: costlayers gateway report\n`);
|
|
1629
|
+
await trackEvent(repo, args, "api_mode_start", { mode: payload.mode, dry_run: payload.dry_run }, connection);
|
|
1004
1630
|
return;
|
|
1005
1631
|
}
|
|
1006
1632
|
if (action === "report") {
|
|
@@ -1020,6 +1646,7 @@ async function gateway(repo, args) {
|
|
|
1020
1646
|
process.stdout.write(`saved_cost_usd: ${summary.saved_cost_usd || 0}\n`);
|
|
1021
1647
|
process.stdout.write(`gateway_authenticated_actions: ${status.gateway_request_count || 0}\n`);
|
|
1022
1648
|
process.stdout.write(`dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
1649
|
+
await trackEvent(repo, args, "gateway_report", {}, connection);
|
|
1023
1650
|
return;
|
|
1024
1651
|
}
|
|
1025
1652
|
if (action === "stop") {
|
|
@@ -1032,8 +1659,20 @@ async function gateway(repo, args) {
|
|
|
1032
1659
|
}
|
|
1033
1660
|
|
|
1034
1661
|
async function dashboard(repo, args) {
|
|
1035
|
-
const connection =
|
|
1036
|
-
|
|
1662
|
+
const connection = loadStoredConnection(repo);
|
|
1663
|
+
if (!connection || !connection.engine_url || !connection.api_key) {
|
|
1664
|
+
if (printLocalDashboard(repo)) return;
|
|
1665
|
+
process.exit(2);
|
|
1666
|
+
}
|
|
1667
|
+
let status;
|
|
1668
|
+
try {
|
|
1669
|
+
status = await postJson(`${connection.engine_url}/v1/me`, {}, connection.api_key);
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
process.stderr.write(`Hosted dashboard unavailable: ${err.message}\n\n`);
|
|
1672
|
+
if (printLocalDashboard(repo)) return;
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
}
|
|
1675
|
+
await trackEvent(repo, args, "dashboard_open", {}, connection);
|
|
1037
1676
|
const dashboardUrl = (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
|
|
1038
1677
|
process.stdout.write(`CostLayers Dashboard\n`);
|
|
1039
1678
|
process.stdout.write(`URL: ${dashboardUrl}\n`);
|
|
@@ -1054,11 +1693,11 @@ async function codexShortcut(repo, args, argv) {
|
|
|
1054
1693
|
const codexTail = codexArgsAfterDash(argv);
|
|
1055
1694
|
const command = codexTail.length > 0 ? codexTail : ["codex"];
|
|
1056
1695
|
const commandToRun = isCodexCommand(command) ? command : ["codex", ...command];
|
|
1057
|
-
const nextArgs = withAutoCodexMode(args);
|
|
1696
|
+
const nextArgs = withAutoCodexMode({ ...args, "ux-compact": true });
|
|
1058
1697
|
if (codexProxyEnabled(nextArgs)) {
|
|
1059
1698
|
process.stdout.write(`CostLayers Codex: API invoice mode explicitly enabled from ${codexProxyApiKeyEnv(nextArgs)}.\n`);
|
|
1060
1699
|
} else {
|
|
1061
|
-
process.stdout.write(`CostLayers Codex: ChatGPT usage-stretch mode.
|
|
1700
|
+
process.stdout.write(`CostLayers Codex: ChatGPT usage-stretch mode. Native Codex auth stays unchanged.\n`);
|
|
1062
1701
|
}
|
|
1063
1702
|
return start(repo, nextArgs, ["start", "--", ...commandToRun]);
|
|
1064
1703
|
}
|
|
@@ -1084,6 +1723,7 @@ async function savingsTest(repo, args) {
|
|
|
1084
1723
|
? args.prompt
|
|
1085
1724
|
: "Analyze this repository. Find the main entry points, data flow, and the 5 files most worth reading. Do not edit files.";
|
|
1086
1725
|
assertCodexProxyApiKey(nextArgs, `npx -y ${INSTALL_SPEC} test --email you@example.com`);
|
|
1726
|
+
await trackEvent(repo, nextArgs, "api_savings_test", {}, loadStoredConnection(repo));
|
|
1087
1727
|
process.stdout.write("CostLayers savings test: running one safe read-only Codex task.\n");
|
|
1088
1728
|
const status = await start(repo, nextArgs, ["start", "--", "codex", "exec", "--sandbox", "read-only", prompt], { returnStatus: true });
|
|
1089
1729
|
process.stdout.write("\nCostLayers savings test report\n");
|
|
@@ -1098,27 +1738,39 @@ async function savingsTest(repo, args) {
|
|
|
1098
1738
|
async function start(repo, args, argv, options = {}) {
|
|
1099
1739
|
const dash = argv.indexOf("--");
|
|
1100
1740
|
const command = dash >= 0 ? argv.slice(dash + 1) : [];
|
|
1741
|
+
const compactOutput = Boolean(args["ux-compact"]);
|
|
1101
1742
|
const codexTelemetryRun = command.length > 0 && isCodexCommand(command) && !codexProxyEnabled(args);
|
|
1102
1743
|
if (command.length > 0 && isCodexCommand(command)) assertCodexProxyApiKey(args);
|
|
1103
1744
|
init(repo, { suppressNext: true });
|
|
1104
1745
|
process.stdout.write(`Scanning repo: ${repo}\n`);
|
|
1105
1746
|
const precomputed = scanToFiles(repo, args);
|
|
1106
1747
|
const { outDir, pack, report } = precomputed;
|
|
1748
|
+
writeAuditReport(outDir, repo, report, args);
|
|
1107
1749
|
process.stdout.write(`CostLayers scan complete\n`);
|
|
1108
|
-
process.stdout.write(`
|
|
1109
|
-
|
|
1750
|
+
if (precomputed.localCacheHit) process.stdout.write(`Local exact cache hit: source hashes unchanged, reused CostLayers artifacts\n`);
|
|
1751
|
+
if (!compactOutput) process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
|
|
1752
|
+
printSavingsSummary(report, { compact: compactOutput });
|
|
1110
1753
|
const connection = await ensureConnection(repo, args);
|
|
1754
|
+
await trackEvent(repo, args, "cli_start", {
|
|
1755
|
+
files_indexed: report.files_indexed,
|
|
1756
|
+
context_tokens_avoided_per_task: report.tokens_avoided_per_repeated_task,
|
|
1757
|
+
local_cache_hit: Boolean(precomputed.localCacheHit)
|
|
1758
|
+
}, connection);
|
|
1111
1759
|
try {
|
|
1112
1760
|
await fetchEnginePlan(connection, repo, pack, report);
|
|
1113
1761
|
process.stdout.write(`Dashboard synced with first-run savings\n`);
|
|
1114
1762
|
} catch (err) {
|
|
1115
1763
|
process.stderr.write(`Dashboard sync delayed; local report is still available: ${err.message}\n`);
|
|
1116
1764
|
}
|
|
1117
|
-
process.stdout.write(`CostLayers connection ready\n`);
|
|
1765
|
+
if (!compactOutput) process.stdout.write(`CostLayers connection ready\n`);
|
|
1118
1766
|
let gatewayBaseUrl = connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key);
|
|
1119
1767
|
if (codexTelemetryRun) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1768
|
+
if (compactOutput) {
|
|
1769
|
+
process.stdout.write(`Mode: ChatGPT-login Codex. Model calls are not routed; CostLayers adds repo context discipline and usage-stretch metering.\n`);
|
|
1770
|
+
} else {
|
|
1771
|
+
process.stdout.write(`ChatGPT-login Codex mode: native Codex provider preserved; model calls are not routed through CostLayers.\n`);
|
|
1772
|
+
process.stdout.write(`What users get: repo context discipline and a usage-stretch meter. This does not reduce a flat ChatGPT subscription invoice.\n`);
|
|
1773
|
+
}
|
|
1122
1774
|
} else {
|
|
1123
1775
|
const providerUrl = typeof args["provider-url"] === "string" ? args["provider-url"] : "https://api.openai.com";
|
|
1124
1776
|
const payload = {
|
|
@@ -1140,30 +1792,47 @@ async function start(repo, args, argv, options = {}) {
|
|
|
1140
1792
|
process.stdout.write(`OpenAI-compatible base URL: ${gatewayBaseUrl}\n`);
|
|
1141
1793
|
if (codexProxyEnabled(args)) {
|
|
1142
1794
|
process.stdout.write(`API invoice mode: Codex will use ${codexProxyApiKeyEnv(args)} through the CostLayers gateway.\n`);
|
|
1795
|
+
await trackEvent(repo, args, "api_mode_start", { mode: payload.mode, from_start: true }, connection);
|
|
1143
1796
|
}
|
|
1144
1797
|
}
|
|
1145
1798
|
process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
1146
|
-
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
1799
|
+
if (!compactOutput) process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
1147
1800
|
process.stdout.write(`Plan: free beta\n`);
|
|
1148
1801
|
const profilePath = writeCodexProfile(connection, args);
|
|
1149
|
-
|
|
1150
|
-
|
|
1802
|
+
if (!compactOutput) {
|
|
1803
|
+
process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
|
|
1804
|
+
process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
|
|
1805
|
+
}
|
|
1151
1806
|
if (command.length > 0) {
|
|
1152
|
-
return runAgent(repo, args, argv, { skipSetup: true, precomputed, returnStatus: options.returnStatus });
|
|
1807
|
+
return runAgent(repo, args, argv, { skipSetup: true, precomputed, returnStatus: options.returnStatus, compactOutput });
|
|
1153
1808
|
}
|
|
1154
|
-
process.stdout.write(`\
|
|
1155
|
-
process.stdout.write(`
|
|
1156
|
-
process.stdout.write(`
|
|
1157
|
-
process.stdout.write(`
|
|
1158
|
-
process.stdout.write(` Prove API savings: npx -y ${INSTALL_SPEC} test --email you@example.com\n`);
|
|
1159
|
-
process.stdout.write(` Or run Codex directly: codex --profile costlayers\n`);
|
|
1160
|
-
process.stdout.write(` View report: npx -y ${INSTALL_SPEC} gateway report\n`);
|
|
1161
|
-
process.stdout.write(` Dashboard: npx -y ${INSTALL_SPEC} dashboard\n`);
|
|
1809
|
+
process.stdout.write(`\nReady.\n`);
|
|
1810
|
+
process.stdout.write(` Run Codex: codex --profile costlayers\n`);
|
|
1811
|
+
process.stdout.write(` Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
1812
|
+
process.stdout.write(` API invoice proof: export OPENAI_API_KEY=sk-proj-... && npx -y ${INSTALL_SPEC} test --email you@example.com\n`);
|
|
1162
1813
|
}
|
|
1163
1814
|
|
|
1164
1815
|
function doctor() {
|
|
1816
|
+
const repo = process.cwd();
|
|
1817
|
+
const outDir = path.join(repo, ".agentspend");
|
|
1818
|
+
const saved = loadStoredConnection(repo);
|
|
1819
|
+
const codexCheck = spawnSync("codex", ["--version"], {
|
|
1820
|
+
encoding: "utf8",
|
|
1821
|
+
shell: process.platform === "win32"
|
|
1822
|
+
});
|
|
1823
|
+
const profilePath = path.join(codexHomeDir(), "costlayers.config.toml");
|
|
1824
|
+
const reportPath = path.join(outDir, "savings-report.json");
|
|
1825
|
+
const connectionCount = fs.existsSync(connectionsDir())
|
|
1826
|
+
? fs.readdirSync(connectionsDir()).filter((name) => name.endsWith(".json")).length
|
|
1827
|
+
: 0;
|
|
1165
1828
|
process.stdout.write(`CostLayers ${VERSION}\n`);
|
|
1166
1829
|
process.stdout.write(`Node ${process.version}\n`);
|
|
1830
|
+
process.stdout.write(`Codex: ${codexCheck.status === 0 ? String(codexCheck.stdout || codexCheck.stderr).trim() : "not found"}\n`);
|
|
1831
|
+
process.stdout.write(`Repo: ${repo}${isHomeDirectory(repo) ? " (home directory; use --repo or cd into a project)" : ""}\n`);
|
|
1832
|
+
process.stdout.write(`Local report: ${fs.existsSync(reportPath) ? reportPath : "not found; run costlayers scan"}\n`);
|
|
1833
|
+
process.stdout.write(`Saved repo connection: ${saved && saved.api_key ? "yes" : "no"}\n`);
|
|
1834
|
+
process.stdout.write(`Private connection store entries: ${connectionCount}\n`);
|
|
1835
|
+
process.stdout.write(`Codex profile: ${fs.existsSync(profilePath) ? profilePath : "not installed"}\n`);
|
|
1167
1836
|
process.stdout.write("Status: ok\n");
|
|
1168
1837
|
}
|
|
1169
1838
|
|
|
@@ -1173,8 +1842,9 @@ function main() {
|
|
|
1173
1842
|
const cmd = args._[0];
|
|
1174
1843
|
if (!cmd || args.help || args.h) usage(0);
|
|
1175
1844
|
const repo = path.resolve(String(args.repo || process.cwd()));
|
|
1176
|
-
if (["init", "scan", "start", "run", "codex-profile", "codex", "test"].includes(cmd)) guardRepoRoot(repo, args);
|
|
1845
|
+
if (["init", "scan", "audit", "start", "run", "codex-profile", "codex", "test"].includes(cmd)) guardRepoRoot(repo, args);
|
|
1177
1846
|
if (cmd === "doctor") return doctor();
|
|
1847
|
+
if (cmd === "audit") return audit(repo, args);
|
|
1178
1848
|
if (cmd === "codex") return codexShortcut(repo, args, rawArgv);
|
|
1179
1849
|
if (cmd === "test") return savingsTest(repo, args);
|
|
1180
1850
|
if (cmd === "init") return init(repo);
|
package/package.json
CHANGED