costlayers 0.8.0
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 +65 -0
- package/bin/agentspend.js +718 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# CostLayers CLI
|
|
2
|
+
|
|
3
|
+
CostLayers helps teams reduce wasted AI coding-agent spend by creating a compact repo context pack, an OpenAI-compatible gateway, and a savings dashboard.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Preview from the live server:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y https://costlayers.com/agentspend-cli-0.8.0.tgz start --email you@example.com
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
After npm publish, the intended short command is:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx costlayers start --email you@example.com
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Inside a repo:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
agentspend init
|
|
25
|
+
agentspend scan
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With the closed engine enabled:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
agentspend signup --email you@example.com
|
|
32
|
+
agentspend gateway start --provider-url https://api.openai.com --mode reduce
|
|
33
|
+
agentspend run -- codex
|
|
34
|
+
agentspend gateway report
|
|
35
|
+
agentspend dashboard
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Output:
|
|
39
|
+
|
|
40
|
+
- `.agentspend/repo-pack.md`
|
|
41
|
+
- `.agentspend/savings-report.md`
|
|
42
|
+
- `.agentspend/savings-report.json`
|
|
43
|
+
- `.agentspend/agent-instructions.md`
|
|
44
|
+
- `AGENTS.md` when the repo does not already have one
|
|
45
|
+
|
|
46
|
+
Then tell your coding agent, or let `AGENTS.md` do it:
|
|
47
|
+
|
|
48
|
+
> Before reading the repo broadly, read `.agentspend/repo-pack.md` and `.agentspend/agent-instructions.md`.
|
|
49
|
+
|
|
50
|
+
## What It Saves
|
|
51
|
+
|
|
52
|
+
The first public version reduces waste by making agents start from a compact repo map instead of repeatedly rediscovering the same project structure.
|
|
53
|
+
|
|
54
|
+
The report estimates:
|
|
55
|
+
|
|
56
|
+
- baseline broad-read tokens
|
|
57
|
+
- AgentSpend context-pack tokens
|
|
58
|
+
- tokens avoided per repeated task
|
|
59
|
+
- estimated cost avoided per 100 repeated tasks
|
|
60
|
+
|
|
61
|
+
No private internals are included in this package.
|
|
62
|
+
|
|
63
|
+
## Closed Engine
|
|
64
|
+
|
|
65
|
+
The npm package is a controller. The stronger reduction engine and cost gateway run separately and are not shipped in the public package.
|
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const http = require("http");
|
|
8
|
+
const https = require("https");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const VERSION = "0.8.0";
|
|
12
|
+
const DEFAULT_EXCLUDES = new Set([
|
|
13
|
+
".git",
|
|
14
|
+
".hg",
|
|
15
|
+
".svn",
|
|
16
|
+
"node_modules",
|
|
17
|
+
"vendor",
|
|
18
|
+
"dist",
|
|
19
|
+
"build",
|
|
20
|
+
"target",
|
|
21
|
+
".next",
|
|
22
|
+
".nuxt",
|
|
23
|
+
".venv",
|
|
24
|
+
"venv",
|
|
25
|
+
"__pycache__",
|
|
26
|
+
".pytest_cache",
|
|
27
|
+
".mypy_cache",
|
|
28
|
+
".ruff_cache",
|
|
29
|
+
".turbo",
|
|
30
|
+
".cache",
|
|
31
|
+
".agentspend"
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
35
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
36
|
+
".py", ".go", ".rs", ".java", ".kt", ".kts", ".swift",
|
|
37
|
+
".cs", ".php", ".rb", ".c", ".h", ".cpp", ".hpp",
|
|
38
|
+
".cc", ".hh", ".dart", ".scala", ".lua", ".vue", ".svelte",
|
|
39
|
+
".md", ".mdx", ".json", ".yaml", ".yml", ".toml", ".sql"
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function usage(exitCode = 0) {
|
|
43
|
+
const text = `
|
|
44
|
+
AgentSpend ${VERSION}
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
agentspend init [--repo <path>]
|
|
48
|
+
agentspend scan [--repo <path>] [--price-per-1m <usd>] [--tasks <n>]
|
|
49
|
+
agentspend start [--email <email>] [--provider-url <url>] [--mode measure|reduce] [-- <agent command>]
|
|
50
|
+
agentspend signup [--email <email>] [--engine-url <url>]
|
|
51
|
+
agentspend connect --engine-url <url> [--api-key <key>]
|
|
52
|
+
agentspend gateway start [--provider-url <url>] [--api-key-env <name>] [--mode measure|reduce]
|
|
53
|
+
agentspend gateway report
|
|
54
|
+
agentspend gateway stop
|
|
55
|
+
agentspend dashboard
|
|
56
|
+
agentspend run [--repo <path>] -- <agent command>
|
|
57
|
+
agentspend doctor
|
|
58
|
+
|
|
59
|
+
Commands:
|
|
60
|
+
init Create .agentspend config and agent instructions.
|
|
61
|
+
scan Build repo context pack and savings report.
|
|
62
|
+
start One-command setup, signup, gateway start, and optional agent run.
|
|
63
|
+
signup Create a self-serve key and save connection settings.
|
|
64
|
+
connect Save closed-engine connection settings.
|
|
65
|
+
gateway Manage the closed cost gateway.
|
|
66
|
+
dashboard Print dashboard URL and account status.
|
|
67
|
+
run Refresh context, fetch an engine plan, and run your coding agent.
|
|
68
|
+
doctor Check local runtime.
|
|
69
|
+
`;
|
|
70
|
+
process.stdout.write(text.trimStart());
|
|
71
|
+
process.exit(exitCode);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseArgs(argv) {
|
|
75
|
+
const args = { _: [] };
|
|
76
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
77
|
+
const item = argv[i];
|
|
78
|
+
if (!item.startsWith("--")) {
|
|
79
|
+
args._.push(item);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const key = item.slice(2);
|
|
83
|
+
const next = argv[i + 1];
|
|
84
|
+
if (!next || next.startsWith("--")) {
|
|
85
|
+
args[key] = true;
|
|
86
|
+
} else {
|
|
87
|
+
args[key] = next;
|
|
88
|
+
i += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return args;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureDir(dir) {
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeIfMissing(file, content) {
|
|
99
|
+
if (!fs.existsSync(file)) fs.writeFileSync(file, content, "utf8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sha256(text) {
|
|
103
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readJsonIfExists(file) {
|
|
107
|
+
if (!fs.existsSync(file)) return null;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function estimateTokens(text) {
|
|
116
|
+
return Math.ceil(String(text || "").length / 4);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function walkFiles(root) {
|
|
120
|
+
const out = [];
|
|
121
|
+
function walk(dir) {
|
|
122
|
+
let entries = [];
|
|
123
|
+
try {
|
|
124
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (DEFAULT_EXCLUDES.has(entry.name)) continue;
|
|
130
|
+
const full = path.join(dir, entry.name);
|
|
131
|
+
if (entry.isDirectory()) {
|
|
132
|
+
walk(full);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!entry.isFile()) continue;
|
|
136
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
137
|
+
if (!SOURCE_EXTENSIONS.has(ext)) continue;
|
|
138
|
+
let stat;
|
|
139
|
+
try {
|
|
140
|
+
stat = fs.statSync(full);
|
|
141
|
+
} catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (stat.size > 512 * 1024) continue;
|
|
145
|
+
out.push({ full, rel: path.relative(root, full).replace(/\\/g, "/"), size: stat.size, ext });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
walk(root);
|
|
149
|
+
return out.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractSignals(file, content) {
|
|
153
|
+
const lines = content.split(/\r?\n/);
|
|
154
|
+
const defs = [];
|
|
155
|
+
const imports = [];
|
|
156
|
+
const routeHints = [];
|
|
157
|
+
const defRe = /^\s*(export\s+)?(async\s+)?(function|class|interface|type|const|let|var)\s+([A-Za-z0-9_$]+)/;
|
|
158
|
+
const pyRe = /^\s*(def|class)\s+([A-Za-z0-9_]+)/;
|
|
159
|
+
const goRe = /^\s*func\s+(\([^)]+\)\s*)?([A-Za-z0-9_]+)/;
|
|
160
|
+
const routeRe = /(app|router)\.(get|post|put|patch|delete)\s*\(|@(Get|Post|Put|Patch|Delete|Controller)\b|path\s*\(|re_path\s*\(/;
|
|
161
|
+
const importRe = /^\s*(import|from|require\(|use\s+|package\s+|using\s+)/;
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
164
|
+
const line = lines[i];
|
|
165
|
+
const d = line.match(defRe) || line.match(pyRe) || line.match(goRe);
|
|
166
|
+
if (d && defs.length < 12) defs.push(`${i + 1}: ${line.trim().slice(0, 140)}`);
|
|
167
|
+
if (importRe.test(line) && imports.length < 8) imports.push(line.trim().slice(0, 140));
|
|
168
|
+
if (routeRe.test(line) && routeHints.length < 8) routeHints.push(`${i + 1}: ${line.trim().slice(0, 140)}`);
|
|
169
|
+
}
|
|
170
|
+
return { defs, imports, routeHints, lineCount: lines.length };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function summarizeRepo(root) {
|
|
174
|
+
const files = walkFiles(root);
|
|
175
|
+
const rows = [];
|
|
176
|
+
const extCounts = new Map();
|
|
177
|
+
let totalTokens = 0;
|
|
178
|
+
|
|
179
|
+
for (const file of files) {
|
|
180
|
+
let content = "";
|
|
181
|
+
try {
|
|
182
|
+
content = fs.readFileSync(file.full, "utf8");
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const tokens = estimateTokens(content);
|
|
187
|
+
totalTokens += tokens;
|
|
188
|
+
extCounts.set(file.ext, (extCounts.get(file.ext) || 0) + 1);
|
|
189
|
+
rows.push({
|
|
190
|
+
...file,
|
|
191
|
+
tokens,
|
|
192
|
+
hash: sha256(content).slice(0, 16),
|
|
193
|
+
signals: extractSignals(file, content)
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rows.sort((a, b) => b.tokens - a.tokens);
|
|
198
|
+
const topFiles = rows.slice(0, 30);
|
|
199
|
+
const routeFiles = rows.filter((r) => r.signals.routeHints.length > 0).slice(0, 20);
|
|
200
|
+
const symbolFiles = rows.filter((r) => r.signals.defs.length > 0).slice(0, 40);
|
|
201
|
+
const extSummary = Array.from(extCounts.entries()).sort((a, b) => b[1] - a[1]);
|
|
202
|
+
|
|
203
|
+
return { files: rows, topFiles, routeFiles, symbolFiles, extSummary, totalTokens };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildRepoPack(repo, summary) {
|
|
207
|
+
const parts = [];
|
|
208
|
+
parts.push("# AgentSpend Repo Pack");
|
|
209
|
+
parts.push("");
|
|
210
|
+
parts.push("Read this before broad repo exploration. Use it to avoid repeatedly rediscovering project structure.");
|
|
211
|
+
parts.push("");
|
|
212
|
+
parts.push(`Repo: ${path.basename(repo)}`);
|
|
213
|
+
parts.push(`Files indexed: ${summary.files.length}`);
|
|
214
|
+
parts.push(`Approx source tokens indexed: ${summary.totalTokens.toLocaleString()}`);
|
|
215
|
+
parts.push("");
|
|
216
|
+
parts.push("## Language / File Mix");
|
|
217
|
+
for (const [ext, count] of summary.extSummary.slice(0, 12)) parts.push(`- ${ext}: ${count}`);
|
|
218
|
+
parts.push("");
|
|
219
|
+
parts.push("## Largest Context Sources");
|
|
220
|
+
for (const row of summary.topFiles.slice(0, 20)) {
|
|
221
|
+
parts.push(`- ${row.rel} (${row.tokens.toLocaleString()} tokens, ${row.signals.lineCount} lines, hash ${row.hash})`);
|
|
222
|
+
}
|
|
223
|
+
parts.push("");
|
|
224
|
+
parts.push("## Entry And Route Hints");
|
|
225
|
+
if (summary.routeFiles.length === 0) {
|
|
226
|
+
parts.push("- No obvious route files found by the public scanner.");
|
|
227
|
+
} else {
|
|
228
|
+
for (const row of summary.routeFiles.slice(0, 12)) {
|
|
229
|
+
parts.push(`- ${row.rel}`);
|
|
230
|
+
for (const hint of row.signals.routeHints.slice(0, 4)) parts.push(` - ${hint}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
parts.push("");
|
|
234
|
+
parts.push("## Symbol Hints");
|
|
235
|
+
for (const row of summary.symbolFiles.slice(0, 18)) {
|
|
236
|
+
parts.push(`- ${row.rel}`);
|
|
237
|
+
for (const def of row.signals.defs.slice(0, 4)) parts.push(` - ${def}`);
|
|
238
|
+
}
|
|
239
|
+
parts.push("");
|
|
240
|
+
parts.push("## Agent Operating Rule");
|
|
241
|
+
parts.push("- Start with this pack before reading many files.");
|
|
242
|
+
parts.push("- Prefer targeted reads of files listed above.");
|
|
243
|
+
parts.push("- If a file hash is unchanged, do not reread it unless the task requires exact code.");
|
|
244
|
+
parts.push("- Update this pack after major repo changes with `agentspend scan`.");
|
|
245
|
+
parts.push("");
|
|
246
|
+
return parts.join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildInstructions() {
|
|
250
|
+
return `# AgentSpend Agent Instructions
|
|
251
|
+
|
|
252
|
+
Before broad repo exploration:
|
|
253
|
+
|
|
254
|
+
1. Read .agentspend/repo-pack.md.
|
|
255
|
+
2. Use the listed entry points, route hints, and symbol hints to target file reads.
|
|
256
|
+
3. Avoid rereading unchanged large files unless exact code is required.
|
|
257
|
+
4. After major repo changes, ask the user to run \`agentspend scan\`.
|
|
258
|
+
|
|
259
|
+
Goal: reduce repeated context spend while preserving answer quality.
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildAgentsMd() {
|
|
264
|
+
return `# Repository Agent Instructions
|
|
265
|
+
|
|
266
|
+
This repo uses AgentSpend to reduce repeated AI coding-agent context spend.
|
|
267
|
+
|
|
268
|
+
Before broad exploration:
|
|
269
|
+
|
|
270
|
+
1. Read .agentspend/repo-pack.md if it exists.
|
|
271
|
+
2. Read .agentspend/agent-instructions.md.
|
|
272
|
+
3. Prefer targeted file reads based on the repo pack.
|
|
273
|
+
4. Avoid rereading unchanged large files unless exact code is required.
|
|
274
|
+
5. After major repo changes, run or ask for \`agentspend scan\`.
|
|
275
|
+
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildReport(summary, repoPack, tasks, pricePer1m) {
|
|
280
|
+
const packTokens = estimateTokens(repoPack);
|
|
281
|
+
const broadReadTokens = summary.topFiles.slice(0, 20).reduce((acc, row) => acc + row.tokens, 0);
|
|
282
|
+
const avoidedPerTask = Math.max(0, broadReadTokens - packTokens);
|
|
283
|
+
const avoidedTotal = avoidedPerTask * tasks;
|
|
284
|
+
const savedUsd = avoidedTotal / 1_000_000 * pricePer1m;
|
|
285
|
+
const reductionPct = broadReadTokens > 0 ? avoidedPerTask / broadReadTokens * 100 : 0;
|
|
286
|
+
return {
|
|
287
|
+
created_utc: new Date().toISOString(),
|
|
288
|
+
files_indexed: summary.files.length,
|
|
289
|
+
source_tokens_indexed: summary.totalTokens,
|
|
290
|
+
context_pack_tokens: packTokens,
|
|
291
|
+
baseline_broad_read_tokens: broadReadTokens,
|
|
292
|
+
tokens_avoided_per_repeated_task: avoidedPerTask,
|
|
293
|
+
repeated_tasks_modeled: tasks,
|
|
294
|
+
tokens_avoided_total: avoidedTotal,
|
|
295
|
+
price_per_1m_input_tokens_usd: pricePer1m,
|
|
296
|
+
estimated_usd_saved: Number(savedUsd.toFixed(4)),
|
|
297
|
+
estimated_reduction_percent: Number(reductionPct.toFixed(2)),
|
|
298
|
+
largest_files: summary.topFiles.slice(0, 10).map((row) => ({
|
|
299
|
+
path: row.rel,
|
|
300
|
+
tokens: row.tokens,
|
|
301
|
+
hash: row.hash
|
|
302
|
+
}))
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function scanToFiles(repo, args) {
|
|
307
|
+
const outDir = path.join(repo, ".agentspend");
|
|
308
|
+
ensureDir(outDir);
|
|
309
|
+
const tasks = Number(args.tasks || 100);
|
|
310
|
+
const pricePer1m = Number(args["price-per-1m"] || 2.0);
|
|
311
|
+
const summary = summarizeRepo(repo);
|
|
312
|
+
const pack = buildRepoPack(repo, summary);
|
|
313
|
+
const report = buildReport(summary, pack, tasks, pricePer1m);
|
|
314
|
+
fs.writeFileSync(path.join(outDir, "repo-pack.md"), pack, "utf8");
|
|
315
|
+
fs.writeFileSync(path.join(outDir, "agent-instructions.md"), buildInstructions(), "utf8");
|
|
316
|
+
fs.writeFileSync(path.join(outDir, "savings-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
317
|
+
fs.writeFileSync(path.join(outDir, "savings-report.md"), reportMarkdown(report), "utf8");
|
|
318
|
+
return { outDir, summary, pack, report };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function reportMarkdown(report) {
|
|
322
|
+
return `# AgentSpend Savings Report
|
|
323
|
+
|
|
324
|
+
Generated: ${report.created_utc}
|
|
325
|
+
|
|
326
|
+
## Result
|
|
327
|
+
|
|
328
|
+
- Files indexed: ${report.files_indexed}
|
|
329
|
+
- Source tokens indexed: ${report.source_tokens_indexed.toLocaleString()}
|
|
330
|
+
- Context-pack tokens: ${report.context_pack_tokens.toLocaleString()}
|
|
331
|
+
- Baseline broad-read tokens: ${report.baseline_broad_read_tokens.toLocaleString()}
|
|
332
|
+
- Tokens avoided per repeated task: ${report.tokens_avoided_per_repeated_task.toLocaleString()}
|
|
333
|
+
- Repeated tasks modeled: ${report.repeated_tasks_modeled}
|
|
334
|
+
- Estimated tokens avoided total: ${report.tokens_avoided_total.toLocaleString()}
|
|
335
|
+
- Estimated reduction: ${report.estimated_reduction_percent}%
|
|
336
|
+
- Estimated saved: $${report.estimated_usd_saved}
|
|
337
|
+
|
|
338
|
+
## How Users Get Value
|
|
339
|
+
|
|
340
|
+
1. Commit or keep \`.agentspend/repo-pack.md\` locally.
|
|
341
|
+
2. Tell the coding agent to read it before broad repo exploration.
|
|
342
|
+
3. Re-run \`agentspend scan\` after major repo changes.
|
|
343
|
+
4. Compare provider usage before and after adopting this workflow.
|
|
344
|
+
|
|
345
|
+
## Largest Repeated Context Sources
|
|
346
|
+
|
|
347
|
+
${report.largest_files.map((row) => `- ${row.path}: ${row.tokens.toLocaleString()} tokens, hash ${row.hash}`).join("\n")}
|
|
348
|
+
|
|
349
|
+
## Caveat
|
|
350
|
+
|
|
351
|
+
This public scanner estimates repeated context waste. Real savings should be validated against provider usage or invoices.
|
|
352
|
+
`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function init(repo) {
|
|
356
|
+
const outDir = path.join(repo, ".agentspend");
|
|
357
|
+
ensureDir(outDir);
|
|
358
|
+
writeIfMissing(path.join(outDir, "config.json"), JSON.stringify({
|
|
359
|
+
version: VERSION,
|
|
360
|
+
created_utc: new Date().toISOString(),
|
|
361
|
+
mode: "public-context-pack"
|
|
362
|
+
}, null, 2) + "\n");
|
|
363
|
+
fs.writeFileSync(path.join(outDir, "agent-instructions.md"), buildInstructions(), "utf8");
|
|
364
|
+
fs.writeFileSync(path.join(outDir, "AGENTS_SNIPPET.md"), buildAgentsMd(), "utf8");
|
|
365
|
+
const agentsPath = path.join(repo, "AGENTS.md");
|
|
366
|
+
if (!fs.existsSync(agentsPath)) {
|
|
367
|
+
fs.writeFileSync(agentsPath, buildAgentsMd(), "utf8");
|
|
368
|
+
process.stdout.write(`Installed AGENTS.md\n`);
|
|
369
|
+
} else {
|
|
370
|
+
process.stdout.write(`AGENTS.md already exists; snippet saved to ${path.join(outDir, "AGENTS_SNIPPET.md")}\n`);
|
|
371
|
+
}
|
|
372
|
+
process.stdout.write(`AgentSpend initialized in ${outDir}\n`);
|
|
373
|
+
process.stdout.write("Next: agentspend scan\n");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function scan(repo, args) {
|
|
377
|
+
const precomputed = scanToFiles(repo, args);
|
|
378
|
+
const { outDir, report } = precomputed;
|
|
379
|
+
process.stdout.write(`AgentSpend scan complete\n`);
|
|
380
|
+
process.stdout.write(`Repo pack: ${path.join(outDir, "repo-pack.md")}\n`);
|
|
381
|
+
process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
|
|
382
|
+
process.stdout.write(`Estimated tokens avoided per repeated task: ${report.tokens_avoided_per_repeated_task.toLocaleString()}\n`);
|
|
383
|
+
process.stdout.write(`Estimated saved per ${report.repeated_tasks_modeled} repeated tasks: $${report.estimated_usd_saved}\n`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function connectEngine(repo, args) {
|
|
387
|
+
const engineUrl = String(args["engine-url"] || "").trim();
|
|
388
|
+
if (!engineUrl) {
|
|
389
|
+
process.stderr.write("Missing --engine-url\n");
|
|
390
|
+
process.exit(2);
|
|
391
|
+
}
|
|
392
|
+
const outDir = path.join(repo, ".agentspend");
|
|
393
|
+
ensureDir(outDir);
|
|
394
|
+
const config = {
|
|
395
|
+
engine_url: engineUrl.replace(/\/+$/, ""),
|
|
396
|
+
api_key: args["api-key"] ? String(args["api-key"]) : null,
|
|
397
|
+
connected_utc: new Date().toISOString()
|
|
398
|
+
};
|
|
399
|
+
fs.writeFileSync(path.join(outDir, "connection.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
400
|
+
process.stdout.write(`Connected AgentSpend engine: ${config.engine_url}\n`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function loadConnection(repo, args) {
|
|
404
|
+
const outDir = path.join(repo, ".agentspend");
|
|
405
|
+
const saved = readJsonIfExists(path.join(outDir, "connection.json")) || {};
|
|
406
|
+
const engineUrl = String(args["engine-url"] || saved.engine_url || "").replace(/\/+$/, "");
|
|
407
|
+
if (!engineUrl) {
|
|
408
|
+
process.stderr.write("Missing engine connection. Run `agentspend connect --engine-url <url>` first.\n");
|
|
409
|
+
process.exit(2);
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
engine_url: engineUrl,
|
|
413
|
+
api_key: args["api-key"] ? String(args["api-key"]) : saved.api_key || null
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function defaultPublicGatewayUrl(engineUrl, apiKey) {
|
|
418
|
+
try {
|
|
419
|
+
const url = new URL(engineUrl);
|
|
420
|
+
if (url.pathname.endsWith("/engine")) {
|
|
421
|
+
url.pathname = url.pathname.slice(0, -"/engine".length) + `/gateway/${encodeURIComponent(apiKey || "")}`;
|
|
422
|
+
return url.toString().replace(/\/+$/, "");
|
|
423
|
+
}
|
|
424
|
+
return `${url.origin}/gateway/${encodeURIComponent(apiKey || "")}`;
|
|
425
|
+
} catch {
|
|
426
|
+
return "";
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function signupConnection(repo, args) {
|
|
431
|
+
const engineUrl = String(args["engine-url"] || "https://costlayers.com/engine").replace(/\/+$/, "");
|
|
432
|
+
const payload = {
|
|
433
|
+
email: args.email || "",
|
|
434
|
+
label: args.label || path.basename(repo),
|
|
435
|
+
public_host: engineUrl.replace(/\/engine$/, "")
|
|
436
|
+
};
|
|
437
|
+
const result = await postJson(`${engineUrl}/v1/register`, payload, null);
|
|
438
|
+
if (!result.ok || !result.api_key) {
|
|
439
|
+
process.stderr.write(`Signup failed: ${JSON.stringify(result, null, 2)}\n`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
const outDir = path.join(repo, ".agentspend");
|
|
443
|
+
ensureDir(outDir);
|
|
444
|
+
const connection = {
|
|
445
|
+
engine_url: result.engine_url || engineUrl,
|
|
446
|
+
api_key: result.api_key,
|
|
447
|
+
gateway_url: result.gateway_url,
|
|
448
|
+
connected_utc: new Date().toISOString()
|
|
449
|
+
};
|
|
450
|
+
fs.writeFileSync(path.join(outDir, "connection.json"), JSON.stringify(connection, null, 2) + "\n", "utf8");
|
|
451
|
+
return connection;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function ensureConnection(repo, args) {
|
|
455
|
+
const outDir = path.join(repo, ".agentspend");
|
|
456
|
+
const saved = readJsonIfExists(path.join(outDir, "connection.json"));
|
|
457
|
+
if (saved && saved.engine_url && saved.api_key) return saved;
|
|
458
|
+
return signupConnection(repo, args);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function signup(repo, args) {
|
|
462
|
+
const connection = await signupConnection(repo, args);
|
|
463
|
+
process.stdout.write(`AgentSpend self-serve key created\n`);
|
|
464
|
+
process.stdout.write(`Engine: ${connection.engine_url}\n`);
|
|
465
|
+
process.stdout.write(`Gateway: ${connection.gateway_url}\n`);
|
|
466
|
+
process.stdout.write(`Plan: free beta\n`);
|
|
467
|
+
process.stdout.write(`Next: agentspend gateway start --mode reduce --provider-url https://api.openai.com\n`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function postJson(urlString, payload, apiKey) {
|
|
471
|
+
return new Promise((resolve, reject) => {
|
|
472
|
+
const url = new URL(urlString);
|
|
473
|
+
const body = JSON.stringify(payload);
|
|
474
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
475
|
+
const req = transport.request({
|
|
476
|
+
method: "POST",
|
|
477
|
+
hostname: url.hostname,
|
|
478
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
479
|
+
path: url.pathname + url.search,
|
|
480
|
+
headers: {
|
|
481
|
+
"content-type": "application/json",
|
|
482
|
+
"content-length": Buffer.byteLength(body),
|
|
483
|
+
...(apiKey ? { "authorization": `Bearer ${apiKey}` } : {})
|
|
484
|
+
},
|
|
485
|
+
timeout: 15000
|
|
486
|
+
}, (res) => {
|
|
487
|
+
let data = "";
|
|
488
|
+
res.setEncoding("utf8");
|
|
489
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
490
|
+
res.on("end", () => {
|
|
491
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
492
|
+
reject(new Error(`engine returned HTTP ${res.statusCode}: ${data.slice(0, 300)}`));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
resolve(JSON.parse(data));
|
|
497
|
+
} catch (err) {
|
|
498
|
+
reject(err);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
req.on("error", reject);
|
|
503
|
+
req.on("timeout", () => {
|
|
504
|
+
req.destroy(new Error("engine request timed out"));
|
|
505
|
+
});
|
|
506
|
+
req.write(body);
|
|
507
|
+
req.end();
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function buildLocalPlan(report) {
|
|
512
|
+
return {
|
|
513
|
+
mode: "local",
|
|
514
|
+
created_utc: new Date().toISOString(),
|
|
515
|
+
plan_summary: "Use the repo pack before broad exploration and avoid rereading unchanged large files.",
|
|
516
|
+
expected_value: {
|
|
517
|
+
tokens_avoided_per_repeated_task: report.tokens_avoided_per_repeated_task,
|
|
518
|
+
estimated_usd_saved: report.estimated_usd_saved
|
|
519
|
+
},
|
|
520
|
+
runtime_instructions: [
|
|
521
|
+
"Read .agentspend/repo-pack.md before broad exploration.",
|
|
522
|
+
"Use .agentspend/savings-report.md to identify repeated context sources.",
|
|
523
|
+
"Prefer targeted reads of files listed in the repo pack.",
|
|
524
|
+
"Do not reread unchanged large files unless exact code is required."
|
|
525
|
+
]
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function writeRuntimePrompt(outDir, plan) {
|
|
530
|
+
const lines = [];
|
|
531
|
+
lines.push("# AgentSpend Runtime Plan");
|
|
532
|
+
lines.push("");
|
|
533
|
+
lines.push(plan.plan_summary || "Use AgentSpend repo context before broad exploration.");
|
|
534
|
+
lines.push("");
|
|
535
|
+
lines.push("## Instructions");
|
|
536
|
+
for (const item of plan.runtime_instructions || []) lines.push(`- ${item}`);
|
|
537
|
+
lines.push("");
|
|
538
|
+
if (plan.expected_value) {
|
|
539
|
+
lines.push("## Expected Value");
|
|
540
|
+
for (const [key, value] of Object.entries(plan.expected_value)) lines.push(`- ${key}: ${value}`);
|
|
541
|
+
lines.push("");
|
|
542
|
+
}
|
|
543
|
+
fs.writeFileSync(path.join(outDir, "runtime-plan.md"), lines.join("\n"), "utf8");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function runAgent(repo, args, argv, options = {}) {
|
|
547
|
+
const dash = argv.indexOf("--");
|
|
548
|
+
const command = dash >= 0 ? argv.slice(dash + 1) : [];
|
|
549
|
+
if (command.length === 0) {
|
|
550
|
+
process.stderr.write("Usage: agentspend run -- <agent command>\n");
|
|
551
|
+
process.exit(2);
|
|
552
|
+
}
|
|
553
|
+
if (!options.skipSetup) init(repo);
|
|
554
|
+
const { outDir, pack, report } = options.precomputed || scanToFiles(repo, args);
|
|
555
|
+
const connection = readJsonIfExists(path.join(outDir, "connection.json"));
|
|
556
|
+
let plan = buildLocalPlan(report);
|
|
557
|
+
if (connection && connection.engine_url) {
|
|
558
|
+
const payload = {
|
|
559
|
+
version: VERSION,
|
|
560
|
+
repo_name: path.basename(repo),
|
|
561
|
+
repo_pack_sha256: sha256(pack),
|
|
562
|
+
repo_pack_preview: pack.slice(0, 12000),
|
|
563
|
+
savings_report: report
|
|
564
|
+
};
|
|
565
|
+
try {
|
|
566
|
+
plan = await postJson(`${connection.engine_url}/v1/plan`, payload, connection.api_key);
|
|
567
|
+
plan.mode = plan.mode || "closed-engine";
|
|
568
|
+
process.stdout.write(`Fetched AgentSpend engine plan\n`);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
process.stderr.write(`Engine unavailable; falling back to local plan: ${err.message}\n`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
writeRuntimePrompt(outDir, plan);
|
|
574
|
+
process.stdout.write(`Runtime plan: ${path.join(outDir, "runtime-plan.md")}\n`);
|
|
575
|
+
const env = {
|
|
576
|
+
...process.env,
|
|
577
|
+
AGENTSPEND_REPO_PACK: path.join(outDir, "repo-pack.md"),
|
|
578
|
+
AGENTSPEND_RUNTIME_PLAN: path.join(outDir, "runtime-plan.md")
|
|
579
|
+
};
|
|
580
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
581
|
+
cwd: repo,
|
|
582
|
+
env,
|
|
583
|
+
stdio: "inherit",
|
|
584
|
+
shell: process.platform === "win32"
|
|
585
|
+
});
|
|
586
|
+
process.exit(typeof result.status === "number" ? result.status : 1);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function gateway(repo, args) {
|
|
590
|
+
const action = args._[1] || "start";
|
|
591
|
+
const connection = loadConnection(repo, args);
|
|
592
|
+
if (action === "start") {
|
|
593
|
+
const payload = {
|
|
594
|
+
host: args.host || "127.0.0.1",
|
|
595
|
+
port: Number(args.port || 8788),
|
|
596
|
+
provider_url: typeof args["provider-url"] === "string" ? args["provider-url"] : "",
|
|
597
|
+
api_key_env: args["api-key-env"] || "OPENAI_API_KEY",
|
|
598
|
+
mode: args.mode || "measure",
|
|
599
|
+
dry_run: Boolean(args["dry-run"]),
|
|
600
|
+
public_gateway_url: args["public-gateway-url"] || connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)
|
|
601
|
+
};
|
|
602
|
+
const result = await postJson(`${connection.engine_url}/v1/gateway/start`, payload, connection.api_key);
|
|
603
|
+
if (!result.ok) {
|
|
604
|
+
process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
process.stdout.write(`AgentSpend gateway ready: ${result.base_url}\n`);
|
|
608
|
+
process.stdout.write(`Set your OpenAI-compatible base URL to: ${result.base_url}\n`);
|
|
609
|
+
process.stdout.write(`Report: agentspend gateway report\n`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (action === "report") {
|
|
613
|
+
const result = await postJson(`${connection.engine_url}/v1/gateway/report`, { port: Number(args.port || 8788) }, connection.api_key);
|
|
614
|
+
if (!result.ok) {
|
|
615
|
+
process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
const summary = result.summary || {};
|
|
619
|
+
process.stdout.write(`AgentSpend Gateway Report\n`);
|
|
620
|
+
process.stdout.write(`requests: ${summary.request_count || 0}\n`);
|
|
621
|
+
process.stdout.write(`provider_called: ${summary["up" + "stream_called"] || 0}\n`);
|
|
622
|
+
process.stdout.write(`provider_avoided: ${summary["up" + "stream_avoided"] || 0}\n`);
|
|
623
|
+
process.stdout.write(`baseline_cost_usd: ${summary.baseline_cost_usd || 0}\n`);
|
|
624
|
+
process.stdout.write(`actual_cost_usd: ${summary.actual_cost_usd || 0}\n`);
|
|
625
|
+
process.stdout.write(`saved_cost_usd: ${summary.saved_cost_usd || 0}\n`);
|
|
626
|
+
process.stdout.write(`report_path: ${result.report_path || ""}\n`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (action === "stop") {
|
|
630
|
+
const result = await postJson(`${connection.engine_url}/v1/gateway/stop`, {}, connection.api_key);
|
|
631
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
process.stderr.write("Usage: agentspend gateway start|report|stop\n");
|
|
635
|
+
process.exit(2);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function dashboard(repo, args) {
|
|
639
|
+
const connection = loadConnection(repo, args);
|
|
640
|
+
const status = await postJson(`${connection.engine_url}/v1/me`, {}, connection.api_key);
|
|
641
|
+
const dashboardUrl = (connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace("/gateway/", "/engine/dashboard/");
|
|
642
|
+
process.stdout.write(`AgentSpend Dashboard\n`);
|
|
643
|
+
process.stdout.write(`URL: ${dashboardUrl}\n`);
|
|
644
|
+
process.stdout.write(`status: ${status.status || "unknown"}\n`);
|
|
645
|
+
process.stdout.write(`plan: ${status.free_beta ? "free beta" : "metered"}\n`);
|
|
646
|
+
process.stdout.write(`metered_savings_usd: ${status.metered_savings_usd ?? ""}\n`);
|
|
647
|
+
process.stdout.write(`gateway_requests: ${status.gateway_request_count ?? 0}\n`);
|
|
648
|
+
process.stdout.write(`blocked_requests: ${status.blocked_request_count ?? 0}\n`);
|
|
649
|
+
process.stdout.write(`rate_limit_per_minute: ${status.rate_limit_per_minute ?? ""}\n`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function start(repo, args, argv) {
|
|
653
|
+
const dash = argv.indexOf("--");
|
|
654
|
+
const command = dash >= 0 ? argv.slice(dash + 1) : [];
|
|
655
|
+
init(repo);
|
|
656
|
+
const precomputed = scanToFiles(repo, args);
|
|
657
|
+
const { outDir, report } = precomputed;
|
|
658
|
+
process.stdout.write(`AgentSpend scan complete\n`);
|
|
659
|
+
process.stdout.write(`Report: ${path.join(outDir, "savings-report.md")}\n`);
|
|
660
|
+
const connection = await ensureConnection(repo, args);
|
|
661
|
+
process.stdout.write(`AgentSpend connection ready\n`);
|
|
662
|
+
const providerUrl = typeof args["provider-url"] === "string" ? args["provider-url"] : "https://api.openai.com";
|
|
663
|
+
const payload = {
|
|
664
|
+
host: args.host || "127.0.0.1",
|
|
665
|
+
port: Number(args.port || 8788),
|
|
666
|
+
provider_url: providerUrl,
|
|
667
|
+
api_key_env: args["api-key-env"] || "OPENAI_API_KEY",
|
|
668
|
+
mode: args.mode || "reduce",
|
|
669
|
+
dry_run: Boolean(args["dry-run"]),
|
|
670
|
+
public_gateway_url: args["public-gateway-url"] || connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)
|
|
671
|
+
};
|
|
672
|
+
const result = await postJson(`${connection.engine_url}/v1/gateway/start`, payload, connection.api_key);
|
|
673
|
+
if (!result.ok) {
|
|
674
|
+
process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
process.stdout.write(`AgentSpend gateway ready: ${result.base_url}\n`);
|
|
678
|
+
process.stdout.write(`OpenAI-compatible base URL: ${result.base_url}\n`);
|
|
679
|
+
process.stdout.write(`Plan: free beta\n`);
|
|
680
|
+
process.stdout.write(`Estimated saved per ${report.repeated_tasks_modeled} repeated tasks: $${report.estimated_usd_saved}\n`);
|
|
681
|
+
if (command.length > 0) {
|
|
682
|
+
return runAgent(repo, args, argv, { skipSetup: true, precomputed });
|
|
683
|
+
}
|
|
684
|
+
process.stdout.write(`\nNext options:\n`);
|
|
685
|
+
process.stdout.write(` Use gateway URL in your model client: ${result.base_url}\n`);
|
|
686
|
+
process.stdout.write(` Or run an agent: npx -y https://costlayers.com/agentspend-cli-${VERSION}.tgz start -- codex\n`);
|
|
687
|
+
process.stdout.write(` View savings: npx -y https://costlayers.com/agentspend-cli-${VERSION}.tgz gateway report\n`);
|
|
688
|
+
process.stdout.write(` Dashboard: npx -y https://costlayers.com/agentspend-cli-${VERSION}.tgz dashboard\n`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function doctor() {
|
|
692
|
+
process.stdout.write(`AgentSpend ${VERSION}\n`);
|
|
693
|
+
process.stdout.write(`Node ${process.version}\n`);
|
|
694
|
+
process.stdout.write("Status: ok\n");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function main() {
|
|
698
|
+
const rawArgv = process.argv.slice(2);
|
|
699
|
+
const args = parseArgs(rawArgv);
|
|
700
|
+
const cmd = args._[0];
|
|
701
|
+
if (!cmd || args.help || args.h) usage(0);
|
|
702
|
+
const repo = path.resolve(String(args.repo || process.cwd()));
|
|
703
|
+
if (cmd === "doctor") return doctor();
|
|
704
|
+
if (cmd === "init") return init(repo);
|
|
705
|
+
if (cmd === "scan") return scan(repo, args);
|
|
706
|
+
if (cmd === "start") return start(repo, args, rawArgv);
|
|
707
|
+
if (cmd === "signup") return signup(repo, args);
|
|
708
|
+
if (cmd === "connect") return connectEngine(repo, args);
|
|
709
|
+
if (cmd === "gateway") return gateway(repo, args);
|
|
710
|
+
if (cmd === "dashboard") return dashboard(repo, args);
|
|
711
|
+
if (cmd === "run") return runAgent(repo, args, rawArgv);
|
|
712
|
+
usage(1);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
Promise.resolve(main()).catch((err) => {
|
|
716
|
+
process.stderr.write(`${err.stack || err.message}\n`);
|
|
717
|
+
process.exit(1);
|
|
718
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "costlayers",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "CostLayers cost control for AI coding agents. Build compact repo context packs, gateway reports, and savings dashboards.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agentspend": "bin/agentspend.js",
|
|
7
|
+
"costlayers": "bin/agentspend.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"license": "UNLICENSED",
|
|
11
|
+
"private": false,
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"smoke": "node bin/agentspend.js doctor && node bin/agentspend.js scan --repo ."
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ai",
|
|
24
|
+
"coding-agent",
|
|
25
|
+
"cost-control",
|
|
26
|
+
"developer-tools",
|
|
27
|
+
"llm",
|
|
28
|
+
"inference"
|
|
29
|
+
]
|
|
30
|
+
}
|