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 +17 -7
- package/bin/agentspend.js +452 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CostLayers CLI
|
|
2
2
|
|
|
3
|
-
CostLayers
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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.
|
|
400
|
-
3.
|
|
401
|
-
4.
|
|
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/
|
|
416
|
-
3.
|
|
417
|
-
4.
|
|
418
|
-
5.
|
|
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
|
-
|
|
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
|
|
629
|
-
const
|
|
630
|
-
|
|
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
|
-
|
|
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:
|
|
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(`\
|
|
1155
|
-
process.stdout.write(`
|
|
1156
|
-
process.stdout.write(`
|
|
1157
|
-
process.stdout.write(`
|
|
1158
|
-
process.stdout.write(` Prove API savings: npx -y ${INSTALL_SPEC} test --email you@example.com\n`);
|
|
1159
|
-
process.stdout.write(` Or run Codex directly: codex --profile costlayers\n`);
|
|
1160
|
-
process.stdout.write(` View report: npx -y ${INSTALL_SPEC} gateway report\n`);
|
|
1161
|
-
process.stdout.write(` Dashboard: npx -y ${INSTALL_SPEC} dashboard\n`);
|
|
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