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 CHANGED
@@ -1,23 +1,23 @@
1
1
  # CostLayers CLI
2
2
 
3
- CostLayers helps coding-agent users stop paying for repeated repo context. API users can route model calls through the gateway for invoice savings. ChatGPT-login Codex users get a usage-stretch meter that shows how much repeated context was avoided.
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
- Daily Codex use:
7
+ Run a free AI spend audit:
8
8
 
9
9
  ```bash
10
10
  cd your-repo
11
- npx -y costlayers@latest codex --email you@example.com --chatgpt
11
+ npx -y https://costlayers.com/costlayers-0.8.27.tgz audit --monthly-spend 10000
12
12
  ```
13
13
 
14
- This default path preserves Codex's native ChatGPT-login/provider flow and shows a usage-stretch meter. CostLayers only routes billable API traffic when you explicitly pass `--api`.
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@latest test --email you@example.com
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@latest codex --email you@example.com --chatgpt
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@latest codex --email you@example.com --api
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@latest codex-profile
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
- const VERSION = "0.8.17";
13
- const INSTALL_SPEC = "costlayers@latest";
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 ensureAgentSpendGitignore(outDir) {
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
- ensureAgentSpendGitignore(outDir);
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. Use the listed entry points, route hints, and symbol hints to target file reads.
400
- 3. Avoid rereading unchanged large files unless exact code is required.
401
- 4. After major repo changes, ask the user to run \`costlayers scan\`.
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/agent-instructions.md.
416
- 3. Prefer targeted file reads based on the repo pack.
417
- 4. Avoid rereading unchanged large files unless exact code is required.
418
- 5. After major repo changes, run or ask for \`costlayers scan\`.
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
- return { outDir, summary, pack, report };
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
- ensureAgentSpendGitignore(outDir);
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 projection = savingsProjection(report);
629
- const avoided = Number(report.tokens_avoided_per_repeated_task || 0);
630
- process.stdout.write(`\n${avoided > 0 ? "CostLayers found repeated context waste" : "CostLayers built your repo context pack"}\n`);
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(`Next: costlayers gateway start --mode reduce --provider-url https://api.openai.com\n`);
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
- repo_pack_preview: pack.slice(0, 12000),
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
- process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
962
- process.stdout.write(`Codex metering enabled: ${commandToRun.join(" ")}\n`);
963
- process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
964
- process.stdout.write(`Savings dashboard: ${dashboardUrlFromConnection(connection)}\n`);
965
- process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
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 = loadConnection(repo, args);
1036
- const status = await postJson(`${connection.engine_url}/v1/me`, {}, connection.api_key);
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. Pass --api to route API-billed provider calls for invoice savings.\n`);
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(`Report: ${path.join(outDir, "savings-report.md")}\n`);
1109
- printSavingsSummary(report);
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
- process.stdout.write(`ChatGPT-login Codex mode: native Codex provider preserved; model calls are not routed through CostLayers.\n`);
1121
- process.stdout.write(`What users get: less repeated repo context and a usage-stretch meter. This does not reduce a flat ChatGPT subscription invoice.\n`);
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
- process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
1150
- process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
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(`\nNext options:\n`);
1155
- process.stdout.write(` ChatGPT-login Codex: npx -y ${INSTALL_SPEC} codex --email you@example.com --chatgpt\n`);
1156
- process.stdout.write(` API invoice Codex: export OPENAI_API_KEY=sk-proj-... && npx -y ${INSTALL_SPEC} codex --email you@example.com --api\n`);
1157
- process.stdout.write(` Other OpenAI-compatible client base URL: ${gatewayBaseUrl}\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "costlayers",
3
- "version": "0.8.17",
3
+ "version": "0.8.27",
4
4
  "description": "CostLayers cost control for AI coding agents. Build compact repo context packs, gateway reports, and savings dashboards.",
5
5
  "bin": {
6
6
  "agentspend": "bin/agentspend.js",