costlayers 0.8.17 → 0.8.20

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,6 +1,6 @@
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 a cost-optimization layer for AI coding agents. It reduces repeated token waste from repo exploration, 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
 
@@ -8,7 +8,7 @@ Daily Codex use:
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.20.tgz codex --email you@example.com --chatgpt
12
12
  ```
13
13
 
14
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`.
@@ -17,7 +17,7 @@ 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.20.tgz test --email you@example.com
21
21
  ```
22
22
 
23
23
  ## Usage
@@ -25,7 +25,7 @@ npx -y costlayers@latest test --email you@example.com
25
25
  Inside a repo:
26
26
 
27
27
  ```bash
28
- npx -y costlayers@latest codex --email you@example.com --chatgpt
28
+ npx -y https://costlayers.com/costlayers-0.8.20.tgz codex --email you@example.com --chatgpt
29
29
  ```
30
30
 
31
31
  This gives Codex `.agentspend/repo-pack.md` and `.agentspend/runtime-plan.md`
@@ -45,7 +45,7 @@ API write permission:
45
45
 
46
46
  ```bash
47
47
  export OPENAI_API_KEY=sk-proj-...
48
- npx -y costlayers@latest codex --email you@example.com --api
48
+ npx -y https://costlayers.com/costlayers-0.8.20.tgz codex --email you@example.com --api
49
49
  ```
50
50
 
51
51
  ChatGPT-login Codex can be metered, but it does not create per-request OpenAI
@@ -61,7 +61,7 @@ Platform invoice savings because it is not billed through your Platform API key.
61
61
  To install only the Codex profile after signup:
62
62
 
63
63
  ```bash
64
- npx -y costlayers@latest codex-profile
64
+ npx -y https://costlayers.com/costlayers-0.8.20.tgz codex-profile
65
65
  codex --profile costlayers
66
66
  ```
67
67
 
@@ -88,6 +88,8 @@ The hosted reducer defaults to quality-safe reduction:
88
88
  - certified compaction preserves the current user request
89
89
  - prior context is compacted only when there is a safe structural boundary
90
90
  - opaque single-message prompts are forwarded unchanged
91
+ - hosted raw provider-response caching is off by default; receipts use hashes,
92
+ token counts, costs, timestamps, and quality labels
91
93
 
92
94
  Output:
93
95
 
@@ -118,4 +120,12 @@ No private internals are included in this package.
118
120
 
119
121
  ## Closed Engine
120
122
 
121
- The npm package is a controller. The stronger reduction engine and cost gateway run separately and are not shipped in the public package.
123
+ 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.
124
+
125
+ ## Privacy Default
126
+
127
+ The CLI scans source locally. When connected, it sends savings reports, artifact
128
+ hashes, and usage metadata so the dashboard can show found waste and metered
129
+ savings. It does not send the repo-pack preview by default. ChatGPT-login Codex
130
+ mode does not route model calls through CostLayers. API invoice mode routes
131
+ 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.20";
15
+ } catch (_err) {
16
+ return "0.8.20";
17
+ }
18
+ }
19
+
20
+ const VERSION = packageVersion();
21
+ const INSTALL_SPEC = "https://costlayers.com/costlayers-0.8.20.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,6 +53,8 @@ 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}
@@ -188,7 +198,7 @@ function repoConnectionPath(repo) {
188
198
  function ensureAgentSpendGitignore(outDir) {
189
199
  ensureDir(outDir);
190
200
  const file = path.join(outDir, ".gitignore");
191
- const required = ["connection.json", "*.secret.json", "gateway-key.txt"];
201
+ const required = ["connection.json", "*.secret.json", "gateway-key.txt", "local-cache.json"];
192
202
  let current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
193
203
  const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
194
204
  let changed = false;
@@ -260,6 +270,12 @@ function estimateTokens(text) {
260
270
  return Math.ceil(String(text || "").length / 4);
261
271
  }
262
272
 
273
+ function stableJson(value) {
274
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
275
+ if (Array.isArray(value)) return `[${value.map((item) => stableJson(item)).join(",")}]`;
276
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
277
+ }
278
+
263
279
  function walkFiles(root) {
264
280
  const out = [];
265
281
  function walk(dir) {
@@ -383,6 +399,7 @@ function buildRepoPack(repo, summary) {
383
399
  parts.push("");
384
400
  parts.push("## Agent Operating Rule");
385
401
  parts.push("- Start with this pack before reading many files.");
402
+ parts.push("- Use .agentspend/semantic-slices.md for route and symbol facts before opening broad source context.");
386
403
  parts.push("- Prefer targeted reads of files listed above.");
387
404
  parts.push("- If a file hash is unchanged, do not reread it unless the task requires exact code.");
388
405
  parts.push("- Update this pack after major repo changes with `costlayers scan`.");
@@ -390,15 +407,194 @@ function buildRepoPack(repo, summary) {
390
407
  return parts.join("\n");
391
408
  }
392
409
 
410
+
411
+ function semanticArea(row) {
412
+ const rel = String(row.rel || "").toLowerCase();
413
+ const signals = row.signals || {};
414
+ if ((signals.routeHints || []).length > 0) return "entrypoints-routes";
415
+ if (/(^|\/)(__tests__|test|tests|spec|specs)(\/|$)/.test(rel) || /\.(test|spec)\.[^.]+$/.test(rel)) return "tests";
416
+ 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";
417
+ if (/\.(md|mdx)$/.test(rel)) return "docs";
418
+ if (/(^|\/)(schema|schemas|model|models|migration|migrations|db|database)(\/|$)/.test(rel) || /\.sql$/.test(rel)) return "data-model";
419
+ if (/(^|\/)(api|server|service|services|controller|controllers|handler|handlers)(\/|$)/.test(rel)) return "services";
420
+ if (/(^|\/)(component|components|page|pages|app|ui|view|views)(\/|$)/.test(rel)) return "ui";
421
+ if (/(^|\/)(cli|bin|cmd|command|commands)(\/|$)/.test(rel)) return "cli-tools";
422
+ return "core";
423
+ }
424
+
425
+ function semanticAreaTitle(area) {
426
+ return ({
427
+ "entrypoints-routes": "Entry Points And Routes",
428
+ "config-build": "Config And Build",
429
+ "data-model": "Data Model",
430
+ "cli-tools": "CLI And Tools",
431
+ services: "Services",
432
+ tests: "Tests",
433
+ docs: "Docs",
434
+ ui: "UI",
435
+ core: "Core"
436
+ })[area] || area;
437
+ }
438
+
439
+ function semanticSymbolName(definitionLine) {
440
+ const text = String(definitionLine || "").replace(/^\d+:\s*/, "").trim();
441
+ const patterns = [
442
+ /(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const|let|var)\s+([A-Za-z0-9_$]+)/,
443
+ /(?:def|class)\s+([A-Za-z0-9_]+)/,
444
+ /func\s+(?:\([^)]+\)\s*)?([A-Za-z0-9_]+)/
445
+ ];
446
+ for (const pattern of patterns) {
447
+ const match = text.match(pattern);
448
+ if (match) return match[1];
449
+ }
450
+ return text.slice(0, 80);
451
+ }
452
+
453
+ function semanticFileFact(row) {
454
+ const signals = row.signals || {};
455
+ const symbols = (signals.defs || []).map(semanticSymbolName).filter(Boolean).slice(0, 12);
456
+ const routes = (signals.routeHints || []).slice(0, 8);
457
+ const imports = (signals.imports || []).slice(0, 6);
458
+ return {
459
+ path: row.rel,
460
+ area: semanticArea(row),
461
+ hash: row.hash,
462
+ tokens: row.tokens,
463
+ lines: signals.lineCount || 0,
464
+ symbols,
465
+ routes,
466
+ imports
467
+ };
468
+ }
469
+
470
+ function semanticFactCount(fact) {
471
+ return 1 + fact.symbols.length + fact.routes.length + Math.min(fact.imports.length, 3);
472
+ }
473
+
474
+ function semanticFactScore(fact) {
475
+ return fact.routes.length * 50 + fact.symbols.length * 8 + fact.imports.length * 2 + Math.min(fact.tokens / 1000, 20);
476
+ }
477
+
478
+ function buildSemanticSlices(repo, summary) {
479
+ const facts = summary.files.map(semanticFileFact);
480
+ const groups = new Map();
481
+ for (const fact of facts) {
482
+ if (!groups.has(fact.area)) groups.set(fact.area, []);
483
+ groups.get(fact.area).push(fact);
484
+ }
485
+ const slices = Array.from(groups.entries()).map(([area, files]) => {
486
+ const sorted = files.slice().sort((a, b) => semanticFactScore(b) - semanticFactScore(a) || b.tokens - a.tokens || a.path.localeCompare(b.path));
487
+ const included = sorted.slice(0, SEMANTIC_SLICE_LIMIT_PER_AREA);
488
+ const sourceTokens = files.reduce((acc, item) => acc + Number(item.tokens || 0), 0);
489
+ const factCount = files.reduce((acc, item) => acc + semanticFactCount(item), 0);
490
+ const sliceSeed = {
491
+ area,
492
+ files: files.map((item) => ({ path: item.path, hash: item.hash, tokens: item.tokens })).sort((a, b) => a.path.localeCompare(b.path))
493
+ };
494
+ return {
495
+ id: `sem_${sha256(stableJson(sliceSeed)).slice(0, 12)}`,
496
+ area,
497
+ title: semanticAreaTitle(area),
498
+ file_count: files.length,
499
+ included_file_count: included.length,
500
+ omitted_file_count: Math.max(0, files.length - included.length),
501
+ source_tokens_covered: sourceTokens,
502
+ fact_count: factCount,
503
+ files: included
504
+ };
505
+ }).sort((a, b) => b.source_tokens_covered - a.source_tokens_covered || a.title.localeCompare(b.title));
506
+ const receiptSeed = {
507
+ artifact_version: 1,
508
+ mode: "offline-semantic-slices",
509
+ repo: path.basename(repo),
510
+ files_indexed: summary.files.length,
511
+ source_tokens_indexed: summary.totalTokens,
512
+ slices: slices.map((slice) => ({
513
+ id: slice.id,
514
+ area: slice.area,
515
+ file_count: slice.file_count,
516
+ source_tokens_covered: slice.source_tokens_covered,
517
+ fact_count: slice.fact_count
518
+ }))
519
+ };
520
+ return {
521
+ artifact_version: 1,
522
+ mode: "offline-semantic-slices",
523
+ created_utc: new Date().toISOString(),
524
+ repo: path.basename(repo),
525
+ files_indexed: summary.files.length,
526
+ source_tokens_indexed: summary.totalTokens,
527
+ file_fact_count: facts.length,
528
+ fact_count: slices.reduce((acc, slice) => acc + slice.fact_count, 0),
529
+ receipt_sha256: sha256(stableJson(receiptSeed)),
530
+ slices
531
+ };
532
+ }
533
+
534
+ function buildSemanticSlicesMarkdown(index) {
535
+ const lines = [];
536
+ lines.push("# CostLayers Semantic Slices");
537
+ lines.push("");
538
+ 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.");
539
+ lines.push("");
540
+ lines.push(`Receipt: ${index.receipt_sha256}`);
541
+ lines.push(`Files indexed: ${index.files_indexed.toLocaleString()}`);
542
+ lines.push(`Source tokens covered: ${index.source_tokens_indexed.toLocaleString()}`);
543
+ lines.push(`Slices: ${index.slices.length.toLocaleString()}`);
544
+ lines.push("");
545
+ for (const slice of index.slices) {
546
+ lines.push(`## ${slice.title}`);
547
+ lines.push(`- Slice id: ${slice.id}`);
548
+ lines.push(`- Files covered: ${slice.file_count.toLocaleString()}`);
549
+ lines.push(`- Source tokens covered: ${slice.source_tokens_covered.toLocaleString()}`);
550
+ if (slice.omitted_file_count > 0) lines.push(`- Additional files in JSON artifact: ${slice.omitted_file_count.toLocaleString()}`);
551
+ for (const file of slice.files.slice(0, 12)) {
552
+ lines.push(`- ${file.path} (${Number(file.tokens || 0).toLocaleString()} tokens, ${Number(file.lines || 0).toLocaleString()} lines, hash ${file.hash})`);
553
+ if (file.routes.length) lines.push(` - routes: ${file.routes.slice(0, 3).join(" | ")}`);
554
+ if (file.symbols.length) lines.push(` - symbols: ${file.symbols.slice(0, 8).join(", ")}`);
555
+ if (file.imports.length) lines.push(` - imports: ${file.imports.slice(0, 3).join(" | ")}`);
556
+ }
557
+ lines.push("");
558
+ }
559
+ return lines.join("\n");
560
+ }
561
+
562
+ function semanticReportSummary(index, semanticMarkdown, baselineBroadReadTokens) {
563
+ const semanticIndexTokens = estimateTokens(semanticMarkdown);
564
+ const potentialAvoided = Math.max(0, Number(baselineBroadReadTokens || 0) - semanticIndexTokens);
565
+ const potentialPct = baselineBroadReadTokens > 0 ? potentialAvoided / baselineBroadReadTokens * 100 : 0;
566
+ return {
567
+ mode: "offline_local_only",
568
+ artifact: ".agentspend/semantic-slices.md",
569
+ json_artifact: ".agentspend/semantic-slices.json",
570
+ receipt_hash: index.receipt_sha256,
571
+ slice_count: index.slices.length,
572
+ file_fact_count: index.file_fact_count,
573
+ fact_count: index.fact_count,
574
+ source_tokens_covered: index.source_tokens_indexed,
575
+ semantic_index_tokens: semanticIndexTokens,
576
+ potential_tokens_avoided_per_repeated_task: potentialAvoided,
577
+ potential_reduction_percent: Number(potentialPct.toFixed(2)),
578
+ top_slices: index.slices.slice(0, 8).map((slice) => ({
579
+ id: slice.id,
580
+ title: slice.title,
581
+ file_count: slice.file_count,
582
+ source_tokens_covered: slice.source_tokens_covered,
583
+ fact_count: slice.fact_count
584
+ }))
585
+ };
586
+ }
587
+
393
588
  function buildInstructions() {
394
589
  return `# CostLayers Agent Instructions
395
590
 
396
591
  Before broad repo exploration:
397
592
 
398
593
  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\`.
594
+ 2. Read .agentspend/semantic-slices.md for route and symbol facts when a compact semantic map is enough.
595
+ 3. Use the listed entry points, route hints, and symbol hints to target file reads.
596
+ 4. Avoid rereading unchanged large files unless exact code is required.
597
+ 5. After major repo changes, ask the user to run \`costlayers scan\`.
402
598
 
403
599
  Goal: reduce repeated context spend while preserving answer quality.
404
600
  `;
@@ -412,15 +608,16 @@ This repo uses CostLayers to reduce repeated AI coding-agent context spend.
412
608
  Before broad exploration:
413
609
 
414
610
  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\`.
611
+ 2. Read .agentspend/semantic-slices.md when route and symbol facts are enough to target the next read.
612
+ 3. Read .agentspend/agent-instructions.md.
613
+ 4. Prefer targeted file reads based on the repo pack.
614
+ 5. Avoid rereading unchanged large files unless exact code is required.
615
+ 6. After major repo changes, run or ask for \`costlayers scan\`.
419
616
 
420
617
  `;
421
618
  }
422
619
 
423
- function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek) {
620
+ function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek, semanticIndex = null, semanticMarkdown = "") {
424
621
  const packTokens = estimateTokens(repoPack);
425
622
  const broadReadTokens = Math.max(
426
623
  summary.totalTokens,
@@ -434,6 +631,7 @@ function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek) {
434
631
  const projectedWeeklyUsd = savedPerRunUsd * runsPerWeek;
435
632
  const projectedMonthlyUsd = projectedWeeklyUsd * WEEKS_PER_MONTH;
436
633
  const reductionPct = broadReadTokens > 0 ? avoidedPerTask / broadReadTokens * 100 : 0;
634
+ const semanticSummary = semanticIndex ? semanticReportSummary(semanticIndex, semanticMarkdown, broadReadTokens) : null;
437
635
  return {
438
636
  created_utc: new Date().toISOString(),
439
637
  files_indexed: summary.files.length,
@@ -453,24 +651,99 @@ function buildReport(summary, repoPack, tasks, pricePer1m, runsPerWeek) {
453
651
  path: row.rel,
454
652
  tokens: row.tokens,
455
653
  hash: row.hash
456
- }))
654
+ })),
655
+ ...(semanticSummary ? { semantic_slices: semanticSummary } : {})
457
656
  };
458
657
  }
459
658
 
659
+ function summaryFingerprint(summary) {
660
+ const files = (summary.files || []).map((row) => ({
661
+ path: row.rel,
662
+ size: row.size,
663
+ hash: row.hash,
664
+ tokens: row.tokens
665
+ })).sort((a, b) => a.path.localeCompare(b.path));
666
+ return sha256(stableJson({ artifact_version: 1, files }));
667
+ }
668
+
669
+ function readLocalCache(outDir) {
670
+ return readJsonIfExists(path.join(outDir, "local-cache.json"));
671
+ }
672
+
673
+ function writeLocalCache(outDir, payload) {
674
+ fs.writeFileSync(path.join(outDir, "local-cache.json"), JSON.stringify(payload, null, 2) + "\n", "utf8");
675
+ }
676
+
460
677
  function scanToFiles(repo, args) {
461
678
  const outDir = path.join(repo, ".agentspend");
462
679
  ensureDir(outDir);
680
+ ensureAgentSpendGitignore(outDir);
463
681
  const tasks = Number(args.tasks || 100);
464
682
  const runsPerWeek = Number(args["runs-per-week"] || DEFAULT_RUNS_PER_WEEK);
465
683
  const pricePer1m = Number(args["price-per-1m"] || 2.0);
466
684
  const summary = summarizeRepo(repo);
685
+ const fingerprint = summaryFingerprint(summary);
686
+ const cached = readLocalCache(outDir);
687
+ if (!args.force && cached && cached.fingerprint === fingerprint) {
688
+ const packFile = path.join(outDir, "repo-pack.md");
689
+ const semanticJsonFile = path.join(outDir, "semantic-slices.json");
690
+ const semanticMdFile = path.join(outDir, "semantic-slices.md");
691
+ const reportFile = path.join(outDir, "savings-report.json");
692
+ if (fs.existsSync(packFile) && fs.existsSync(semanticJsonFile) && fs.existsSync(semanticMdFile) && fs.existsSync(reportFile)) {
693
+ return {
694
+ outDir,
695
+ summary,
696
+ pack: fs.readFileSync(packFile, "utf8"),
697
+ report: readJsonIfExists(reportFile),
698
+ semanticIndex: readJsonIfExists(semanticJsonFile),
699
+ semanticMarkdown: fs.readFileSync(semanticMdFile, "utf8"),
700
+ localCacheHit: true,
701
+ fingerprint
702
+ };
703
+ }
704
+ }
705
+ const semanticIndex = buildSemanticSlices(repo, summary);
706
+ const semanticMarkdown = buildSemanticSlicesMarkdown(semanticIndex);
467
707
  const pack = buildRepoPack(repo, summary);
468
- const report = buildReport(summary, pack, tasks, pricePer1m, runsPerWeek);
708
+ const report = buildReport(summary, pack, tasks, pricePer1m, runsPerWeek, semanticIndex, semanticMarkdown);
469
709
  fs.writeFileSync(path.join(outDir, "repo-pack.md"), pack, "utf8");
710
+ fs.writeFileSync(path.join(outDir, "semantic-slices.json"), JSON.stringify(semanticIndex, null, 2) + "\n", "utf8");
711
+ fs.writeFileSync(path.join(outDir, "semantic-slices.md"), semanticMarkdown, "utf8");
470
712
  fs.writeFileSync(path.join(outDir, "agent-instructions.md"), buildInstructions(), "utf8");
471
713
  fs.writeFileSync(path.join(outDir, "savings-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8");
472
714
  fs.writeFileSync(path.join(outDir, "savings-report.md"), reportMarkdown(report), "utf8");
473
- return { outDir, summary, pack, report };
715
+ writeLocalCache(outDir, {
716
+ artifact_version: 1,
717
+ fingerprint,
718
+ created_utc: new Date().toISOString(),
719
+ files_indexed: summary.files.length,
720
+ source_tokens_indexed: summary.totalTokens,
721
+ repo_pack_sha256: sha256(pack),
722
+ report_sha256: sha256(stableJson(report)),
723
+ semantic_receipt_sha256: semanticIndex.receipt_sha256
724
+ });
725
+ return { outDir, summary, pack, report, semanticIndex, semanticMarkdown, localCacheHit: false, fingerprint };
726
+ }
727
+
728
+
729
+ function semanticReportMarkdown(report) {
730
+ const semantic = report.semantic_slices;
731
+ if (!semantic || !semantic.slice_count) return "";
732
+ const top = Array.isArray(semantic.top_slices) ? semantic.top_slices : [];
733
+ return `## Offline Semantic Slices
734
+
735
+ - Artifact: \`${semantic.artifact}\`
736
+ - JSON: \`${semantic.json_artifact}\`
737
+ - Receipt hash: \`${semantic.receipt_hash}\`
738
+ - Slice count: ${Number(semantic.slice_count || 0).toLocaleString()}
739
+ - Semantic index tokens: ${Number(semantic.semantic_index_tokens || 0).toLocaleString()}
740
+ - Potential tokens avoided per repeated task: ${Number(semantic.potential_tokens_avoided_per_repeated_task || 0).toLocaleString()}
741
+ - Potential reduction vs broad read: ${semantic.potential_reduction_percent}%
742
+
743
+ ${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")}
744
+
745
+ 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.
746
+ `;
474
747
  }
475
748
 
476
749
  function reportMarkdown(report) {
@@ -504,7 +777,7 @@ Generated: ${report.created_utc}
504
777
 
505
778
  ${report.largest_files.map((row) => `- ${row.path}: ${row.tokens.toLocaleString()} tokens, hash ${row.hash}`).join("\n")}
506
779
 
507
- ## Caveat
780
+ ${semanticReportMarkdown(report)}## Caveat
508
781
 
509
782
  This public scanner estimates repeated context waste. Real savings should be validated against provider usage or invoices.
510
783
  `;
@@ -532,14 +805,21 @@ function init(repo, options = {}) {
532
805
  if (!options.suppressNext) process.stdout.write("Next: costlayers scan\n");
533
806
  }
534
807
 
535
- function scan(repo, args) {
808
+ async function scan(repo, args) {
536
809
  process.stdout.write(`Scanning repo: ${repo}\n`);
537
810
  const precomputed = scanToFiles(repo, args);
538
811
  const { outDir, report } = precomputed;
539
812
  process.stdout.write(`CostLayers scan complete\n`);
813
+ if (precomputed.localCacheHit) process.stdout.write(`Local exact cache hit: source hashes unchanged, reused CostLayers artifacts\n`);
540
814
  process.stdout.write(`Repo pack: ${path.join(outDir, "repo-pack.md")}\n`);
815
+ process.stdout.write(`Semantic slices: ${path.join(outDir, "semantic-slices.md")}\n`);
541
816
  process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
542
817
  printSavingsSummary(report);
818
+ await trackEvent(repo, args, "cli_scan", {
819
+ files_indexed: report.files_indexed,
820
+ context_tokens_avoided_per_task: report.tokens_avoided_per_repeated_task,
821
+ local_cache_hit: Boolean(precomputed.localCacheHit)
822
+ });
543
823
  }
544
824
 
545
825
  function connectEngine(repo, args) {
@@ -620,14 +900,35 @@ function savingsProjection(report) {
620
900
  };
621
901
  }
622
902
 
903
+ function savingsVerdict(report) {
904
+ const projection = savingsProjection(report);
905
+ const avoided = Number(report.tokens_avoided_per_repeated_task || 0);
906
+ const sourceTokens = Number(report.source_tokens_indexed || 0);
907
+ const reduction = Number(report.estimated_reduction_percent || 0);
908
+ const meaningful = avoided >= 1000 && reduction >= 15 && projection.monthlyUsd >= 0.25;
909
+ return {
910
+ projection,
911
+ avoided,
912
+ sourceTokens,
913
+ reduction,
914
+ meaningful,
915
+ smallRepo: sourceTokens < 5000 || avoided < 1000
916
+ };
917
+ }
918
+
623
919
  function dashboardUrlFromConnection(connection) {
624
920
  return (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
625
921
  }
626
922
 
627
923
  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`);
924
+ const verdict = savingsVerdict(report);
925
+ const projection = verdict.projection;
926
+ const avoided = verdict.avoided;
927
+ if (!verdict.meaningful) {
928
+ process.stdout.write(`\nCostLayers installed. This repo is too small for a meaningful savings claim.\n`);
929
+ } else {
930
+ process.stdout.write(`\nCostLayers found repeated context waste\n`);
931
+ }
631
932
  process.stdout.write(` Evidence: context estimate from local repo scan\n`);
632
933
  process.stdout.write(` Tokens avoided per repeated task: ${formatInt(report.tokens_avoided_per_repeated_task)}\n`);
633
934
  if (avoided > 0) {
@@ -638,9 +939,16 @@ function printSavingsSummary(report) {
638
939
  }
639
940
  process.stdout.write(` Estimated waste value per ${formatInt(report.repeated_tasks_modeled)} repeated tasks: ${formatUsd(report.estimated_usd_saved)}\n`);
640
941
  process.stdout.write(` Usage-stretch estimate at ${formatInt(projection.runsPerWeek)} agent runs/week: ${formatUsd(projection.weeklyUsd)}/week, ${formatUsd(projection.monthlyUsd)}/month\n`);
942
+ if (!verdict.meaningful) {
943
+ process.stdout.write(` Verdict: no savings claim for this repo. CostLayers is strongest on large repos and repeated agent sessions.\n`);
944
+ }
641
945
  process.stdout.write(` Invoice savings require API traffic through CostLayers invoice mode.\n`);
642
946
  process.stdout.write(` Source tokens indexed: ${formatInt(report.source_tokens_indexed)}\n`);
643
947
  process.stdout.write(` Compact repo pack: ${formatInt(report.context_pack_tokens)} tokens\n`);
948
+ const semantic = report.semantic_slices || null;
949
+ if (semantic && semantic.slice_count) {
950
+ 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`);
951
+ }
644
952
  }
645
953
 
646
954
  function codexHomeDir() {
@@ -698,6 +1006,15 @@ function profileTomlString(connection, args = {}) {
698
1006
  const baseUrl = `${gateway}/v1`;
699
1007
  const engineUrl = String(connection.engine_url || "https://costlayers.com/engine").replace(/\/+$/, "");
700
1008
  const apiKeyEnv = codexProxyApiKeyEnv(args);
1009
+ const otelExporter = {
1010
+ "otlp-http": {
1011
+ endpoint: `${engineUrl}/v1/codex-meter`,
1012
+ protocol: "json",
1013
+ headers: {
1014
+ "x-costlayers-key": connection.api_key || ""
1015
+ }
1016
+ }
1017
+ };
701
1018
  const lines = [
702
1019
  "# Generated by CostLayers. This profile sends Codex telemetry to the CostLayers meter.",
703
1020
  "# Keep this file private because it contains your keyed CostLayers endpoint.",
@@ -723,18 +1040,27 @@ function profileTomlString(connection, args = {}) {
723
1040
  "[otel]",
724
1041
  'environment = "costlayers"',
725
1042
  "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 || "")}`,
1043
+ `exporter = ${toTomlInline(otelExporter)}`,
733
1044
  ""
734
1045
  );
735
1046
  return lines.join("\n");
736
1047
  }
737
1048
 
1049
+ function toTomlInline(value) {
1050
+ if (value === null || value === undefined) return '""';
1051
+ if (Array.isArray(value)) return `[${value.map((item) => toTomlInline(item)).join(", ")}]`;
1052
+ if (typeof value === "object") {
1053
+ return `{ ${Object.entries(value).map(([key, item]) => `${tomlKey(key)} = ${toTomlInline(item)}`).join(", ")} }`;
1054
+ }
1055
+ if (typeof value === "boolean") return value ? "true" : "false";
1056
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1057
+ return JSON.stringify(String(value));
1058
+ }
1059
+
1060
+ function tomlKey(key) {
1061
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
1062
+ }
1063
+
738
1064
  function writeCodexProfile(connection, args = {}) {
739
1065
  const dir = codexHomeDir();
740
1066
  ensureDir(dir);
@@ -821,6 +1147,7 @@ async function ensureConnection(repo, args) {
821
1147
 
822
1148
  async function signup(repo, args) {
823
1149
  const connection = await signupConnection(repo, args);
1150
+ await trackEvent(repo, args, "signup", { label: connection.label || path.basename(repo) }, connection);
824
1151
  process.stdout.write(`CostLayers self-serve key created\n`);
825
1152
  process.stdout.write(`Engine: ${connection.engine_url}\n`);
826
1153
  process.stdout.write(`Gateway: ${connection.gateway_url}\n`);
@@ -833,6 +1160,7 @@ async function signup(repo, args) {
833
1160
  async function codexProfile(repo, args) {
834
1161
  const connection = await ensureConnection(repo, args);
835
1162
  const profilePath = writeCodexProfile(connection, args);
1163
+ await trackEvent(repo, args, "codex_profile", { profile_mode: codexProxyEnabled(args) ? "api" : "chatgpt" }, connection);
836
1164
  process.stdout.write(`CostLayers Codex profile installed\n`);
837
1165
  process.stdout.write(`Profile: ${profilePath}\n`);
838
1166
  process.stdout.write(`Mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode, native Codex provider preserved"}\n`);
@@ -883,38 +1211,91 @@ function postJson(urlString, payload, apiKey) {
883
1211
  });
884
1212
  }
885
1213
 
1214
+ function engineUrlFromArgsOrConnection(args = {}, connection = null) {
1215
+ const fromArgs = String(args["engine-url"] || "").replace(/\/+$/, "");
1216
+ if (fromArgs) return fromArgs;
1217
+ const fromConnection = connection && connection.engine_url ? String(connection.engine_url).replace(/\/+$/, "") : "";
1218
+ return fromConnection || "https://costlayers.com/engine";
1219
+ }
1220
+
1221
+ async function trackEvent(repo, args = {}, eventType, metadata = {}, connection = null) {
1222
+ if (args["no-telemetry"] || process.env.COSTLAYERS_DISABLE_TELEMETRY) return;
1223
+ const engineUrl = engineUrlFromArgsOrConnection(args, connection);
1224
+ const payload = {
1225
+ event_type: eventType,
1226
+ source: "costlayers-cli",
1227
+ email: normalizedEmail(args.email),
1228
+ metadata: {
1229
+ version: VERSION,
1230
+ repo_label: path.basename(repo || process.cwd()),
1231
+ command: args._ ? args._[0] : "",
1232
+ api_mode: apiInvoiceModeRequested(args),
1233
+ chatgpt_mode: chatgptModeRequested(args),
1234
+ platform: process.platform,
1235
+ node: process.version,
1236
+ ...metadata
1237
+ }
1238
+ };
1239
+ try {
1240
+ await postJson(`${engineUrl}/v1/event`, payload, connection ? connection.api_key : null);
1241
+ } catch {
1242
+ // Metrics must never block local developer workflow.
1243
+ }
1244
+ }
1245
+
886
1246
  function buildLocalPlan(report) {
1247
+ const semantic = report.semantic_slices || null;
1248
+ const instructions = [
1249
+ "Read .agentspend/repo-pack.md before broad exploration.",
1250
+ "Use .agentspend/savings-report.md to identify repeated context sources.",
1251
+ "Prefer targeted reads of files listed in the repo pack.",
1252
+ "Do not reread unchanged large files unless exact code is required."
1253
+ ];
1254
+ if (semantic && semantic.slice_count) {
1255
+ instructions.splice(1, 0, "Read .agentspend/semantic-slices.md first when route or symbol facts are enough; open exact files only when needed.");
1256
+ }
887
1257
  return {
888
1258
  mode: "local",
889
1259
  created_utc: new Date().toISOString(),
890
1260
  plan_summary: "Use the repo pack before broad exploration and avoid rereading unchanged large files.",
891
1261
  expected_value: {
892
1262
  tokens_avoided_per_repeated_task: report.tokens_avoided_per_repeated_task,
893
- estimated_usd_saved: report.estimated_usd_saved
1263
+ estimated_usd_saved: report.estimated_usd_saved,
1264
+ semantic_slice_count: semantic ? semantic.slice_count : 0,
1265
+ semantic_index_tokens: semantic ? semantic.semantic_index_tokens : 0,
1266
+ semantic_receipt_hash: semantic ? semantic.receipt_hash : ""
894
1267
  },
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
- ]
1268
+ runtime_instructions: instructions
901
1269
  };
902
1270
  }
903
1271
 
904
1272
  async function fetchEnginePlan(connection, repo, pack, report) {
905
1273
  if (!connection || !connection.engine_url) return null;
1274
+ const semantic = report && report.semantic_slices ? report.semantic_slices : {};
906
1275
  const payload = {
907
1276
  version: VERSION,
908
1277
  repo_name: path.basename(repo),
909
1278
  repo_pack_sha256: sha256(pack),
910
- repo_pack_preview: pack.slice(0, 12000),
911
- savings_report: report
1279
+ privacy_mode: argsPrivacyMode(connection),
1280
+ savings_report: report,
1281
+ local_artifacts: {
1282
+ repo_pack_sha256: sha256(pack),
1283
+ savings_report_sha256: sha256(stableJson(report || {})),
1284
+ semantic_receipt_hash: semantic && semantic.receipt_hash ? String(semantic.receipt_hash) : ""
1285
+ }
912
1286
  };
1287
+ if (connection && connection.send_repo_preview) payload.repo_pack_preview = pack.slice(0, 12000);
913
1288
  const plan = await postJson(`${connection.engine_url}/v1/plan`, payload, connection.api_key);
914
1289
  plan.mode = plan.mode || "closed-engine";
915
1290
  return plan;
916
1291
  }
917
1292
 
1293
+ function argsPrivacyMode(connection) {
1294
+ return connection && connection.send_repo_preview
1295
+ ? "opt_in_repo_preview"
1296
+ : "hashes_metrics_and_reports_default";
1297
+ }
1298
+
918
1299
  function writeRuntimePrompt(outDir, plan) {
919
1300
  const lines = [];
920
1301
  lines.push("# CostLayers Runtime Plan");
@@ -958,6 +1339,7 @@ async function runAgent(repo, args, argv, options = {}) {
958
1339
  assertCodexProxyApiKey(args);
959
1340
  const profilePath = writeCodexProfile(connection, args);
960
1341
  commandToRun = withCostLayersCodexProfile(command);
1342
+ await trackEvent(repo, args, "codex_run", { profile_mode: codexProxyEnabled(args) ? "api" : "chatgpt" }, connection);
961
1343
  process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
962
1344
  process.stdout.write(`Codex metering enabled: ${commandToRun.join(" ")}\n`);
963
1345
  process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
@@ -967,6 +1349,7 @@ async function runAgent(repo, args, argv, options = {}) {
967
1349
  const env = {
968
1350
  ...process.env,
969
1351
  AGENTSPEND_REPO_PACK: path.join(outDir, "repo-pack.md"),
1352
+ AGENTSPEND_SEMANTIC_SLICES: path.join(outDir, "semantic-slices.md"),
970
1353
  AGENTSPEND_RUNTIME_PLAN: path.join(outDir, "runtime-plan.md")
971
1354
  };
972
1355
  const result = spawnSync(commandToRun[0], commandToRun.slice(1), {
@@ -1001,6 +1384,7 @@ async function gateway(repo, args) {
1001
1384
  process.stdout.write(`CostLayers gateway ready: ${result.base_url}\n`);
1002
1385
  process.stdout.write(`Set your OpenAI-compatible base URL to: ${result.base_url}\n`);
1003
1386
  process.stdout.write(`Report: costlayers gateway report\n`);
1387
+ await trackEvent(repo, args, "api_mode_start", { mode: payload.mode, dry_run: payload.dry_run }, connection);
1004
1388
  return;
1005
1389
  }
1006
1390
  if (action === "report") {
@@ -1020,6 +1404,7 @@ async function gateway(repo, args) {
1020
1404
  process.stdout.write(`saved_cost_usd: ${summary.saved_cost_usd || 0}\n`);
1021
1405
  process.stdout.write(`gateway_authenticated_actions: ${status.gateway_request_count || 0}\n`);
1022
1406
  process.stdout.write(`dashboard: ${dashboardUrlFromConnection(connection)}\n`);
1407
+ await trackEvent(repo, args, "gateway_report", {}, connection);
1023
1408
  return;
1024
1409
  }
1025
1410
  if (action === "stop") {
@@ -1034,6 +1419,7 @@ async function gateway(repo, args) {
1034
1419
  async function dashboard(repo, args) {
1035
1420
  const connection = loadConnection(repo, args);
1036
1421
  const status = await postJson(`${connection.engine_url}/v1/me`, {}, connection.api_key);
1422
+ await trackEvent(repo, args, "dashboard_open", {}, connection);
1037
1423
  const dashboardUrl = (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
1038
1424
  process.stdout.write(`CostLayers Dashboard\n`);
1039
1425
  process.stdout.write(`URL: ${dashboardUrl}\n`);
@@ -1084,6 +1470,7 @@ async function savingsTest(repo, args) {
1084
1470
  ? args.prompt
1085
1471
  : "Analyze this repository. Find the main entry points, data flow, and the 5 files most worth reading. Do not edit files.";
1086
1472
  assertCodexProxyApiKey(nextArgs, `npx -y ${INSTALL_SPEC} test --email you@example.com`);
1473
+ await trackEvent(repo, nextArgs, "api_savings_test", {}, loadStoredConnection(repo));
1087
1474
  process.stdout.write("CostLayers savings test: running one safe read-only Codex task.\n");
1088
1475
  const status = await start(repo, nextArgs, ["start", "--", "codex", "exec", "--sandbox", "read-only", prompt], { returnStatus: true });
1089
1476
  process.stdout.write("\nCostLayers savings test report\n");
@@ -1105,9 +1492,15 @@ async function start(repo, args, argv, options = {}) {
1105
1492
  const precomputed = scanToFiles(repo, args);
1106
1493
  const { outDir, pack, report } = precomputed;
1107
1494
  process.stdout.write(`CostLayers scan complete\n`);
1495
+ if (precomputed.localCacheHit) process.stdout.write(`Local exact cache hit: source hashes unchanged, reused CostLayers artifacts\n`);
1108
1496
  process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
1109
1497
  printSavingsSummary(report);
1110
1498
  const connection = await ensureConnection(repo, args);
1499
+ await trackEvent(repo, args, "cli_start", {
1500
+ files_indexed: report.files_indexed,
1501
+ context_tokens_avoided_per_task: report.tokens_avoided_per_repeated_task,
1502
+ local_cache_hit: Boolean(precomputed.localCacheHit)
1503
+ }, connection);
1111
1504
  try {
1112
1505
  await fetchEnginePlan(connection, repo, pack, report);
1113
1506
  process.stdout.write(`Dashboard synced with first-run savings\n`);
@@ -1118,7 +1511,7 @@ async function start(repo, args, argv, options = {}) {
1118
1511
  let gatewayBaseUrl = connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key);
1119
1512
  if (codexTelemetryRun) {
1120
1513
  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`);
1514
+ process.stdout.write(`What users get: repo context discipline and a usage-stretch meter. This does not reduce a flat ChatGPT subscription invoice.\n`);
1122
1515
  } else {
1123
1516
  const providerUrl = typeof args["provider-url"] === "string" ? args["provider-url"] : "https://api.openai.com";
1124
1517
  const payload = {
@@ -1140,6 +1533,7 @@ async function start(repo, args, argv, options = {}) {
1140
1533
  process.stdout.write(`OpenAI-compatible base URL: ${gatewayBaseUrl}\n`);
1141
1534
  if (codexProxyEnabled(args)) {
1142
1535
  process.stdout.write(`API invoice mode: Codex will use ${codexProxyApiKeyEnv(args)} through the CostLayers gateway.\n`);
1536
+ await trackEvent(repo, args, "api_mode_start", { mode: payload.mode, from_start: true }, connection);
1143
1537
  }
1144
1538
  }
1145
1539
  process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
@@ -1151,19 +1545,33 @@ async function start(repo, args, argv, options = {}) {
1151
1545
  if (command.length > 0) {
1152
1546
  return runAgent(repo, args, argv, { skipSetup: true, precomputed, returnStatus: options.returnStatus });
1153
1547
  }
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`);
1548
+ process.stdout.write(`\nReady.\n`);
1549
+ process.stdout.write(` Run Codex: codex --profile costlayers\n`);
1550
+ process.stdout.write(` Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
1551
+ process.stdout.write(` API invoice proof: export OPENAI_API_KEY=sk-proj-... && npx -y ${INSTALL_SPEC} test --email you@example.com\n`);
1162
1552
  }
1163
1553
 
1164
1554
  function doctor() {
1555
+ const repo = process.cwd();
1556
+ const outDir = path.join(repo, ".agentspend");
1557
+ const saved = loadStoredConnection(repo);
1558
+ const codexCheck = spawnSync("codex", ["--version"], {
1559
+ encoding: "utf8",
1560
+ shell: process.platform === "win32"
1561
+ });
1562
+ const profilePath = path.join(codexHomeDir(), "costlayers.config.toml");
1563
+ const reportPath = path.join(outDir, "savings-report.json");
1564
+ const connectionCount = fs.existsSync(connectionsDir())
1565
+ ? fs.readdirSync(connectionsDir()).filter((name) => name.endsWith(".json")).length
1566
+ : 0;
1165
1567
  process.stdout.write(`CostLayers ${VERSION}\n`);
1166
1568
  process.stdout.write(`Node ${process.version}\n`);
1569
+ process.stdout.write(`Codex: ${codexCheck.status === 0 ? String(codexCheck.stdout || codexCheck.stderr).trim() : "not found"}\n`);
1570
+ process.stdout.write(`Repo: ${repo}${isHomeDirectory(repo) ? " (home directory; use --repo or cd into a project)" : ""}\n`);
1571
+ process.stdout.write(`Local report: ${fs.existsSync(reportPath) ? reportPath : "not found; run costlayers scan"}\n`);
1572
+ process.stdout.write(`Saved repo connection: ${saved && saved.api_key ? "yes" : "no"}\n`);
1573
+ process.stdout.write(`Private connection store entries: ${connectionCount}\n`);
1574
+ process.stdout.write(`Codex profile: ${fs.existsSync(profilePath) ? profilePath : "not installed"}\n`);
1167
1575
  process.stdout.write("Status: ok\n");
1168
1576
  }
1169
1577
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "costlayers",
3
- "version": "0.8.17",
3
+ "version": "0.8.20",
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",