costly 0.1.0 → 0.1.1
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/dist/cli.cjs +342 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +40 -23
- package/dist/index.cjs +4 -5
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -5
- package/package.json +3 -2
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var fs = __toESM(require("fs"), 1);
|
|
28
|
+
var path = __toESM(require("path"), 1);
|
|
29
|
+
var readline = __toESM(require("readline"), 1);
|
|
30
|
+
var CYAN = "\x1B[36m";
|
|
31
|
+
var GREEN = "\x1B[32m";
|
|
32
|
+
var DIM = "\x1B[2m";
|
|
33
|
+
var RED = "\x1B[31m";
|
|
34
|
+
var YELLOW = "\x1B[33m";
|
|
35
|
+
var RESET = "\x1B[0m";
|
|
36
|
+
var BOLD = "\x1B[1m";
|
|
37
|
+
var VERSION = "0.1.1";
|
|
38
|
+
var VERIFY_URL = "https://www.getcostly.dev/api/v1/verify-key";
|
|
39
|
+
function log(msg) {
|
|
40
|
+
console.log(msg);
|
|
41
|
+
}
|
|
42
|
+
function success(msg) {
|
|
43
|
+
log(`${GREEN} \u2713${RESET} ${msg}`);
|
|
44
|
+
}
|
|
45
|
+
function warn(msg) {
|
|
46
|
+
log(`${YELLOW} !${RESET} ${msg}`);
|
|
47
|
+
}
|
|
48
|
+
function error(msg) {
|
|
49
|
+
log(`${RED} \u2717${RESET} ${msg}`);
|
|
50
|
+
}
|
|
51
|
+
function dim(msg) {
|
|
52
|
+
return `${DIM}${msg}${RESET}`;
|
|
53
|
+
}
|
|
54
|
+
function ask(question) {
|
|
55
|
+
const rl = readline.createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stdout
|
|
58
|
+
});
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
rl.question(question, (answer) => {
|
|
61
|
+
rl.close();
|
|
62
|
+
resolve(answer.trim());
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function confirm(question) {
|
|
67
|
+
const answer = await ask(`${question} ${dim("(Y/n)")} `);
|
|
68
|
+
return answer === "" || answer.toLowerCase() === "y";
|
|
69
|
+
}
|
|
70
|
+
function detectPackageManager(cwd) {
|
|
71
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
72
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock")))
|
|
73
|
+
return "bun";
|
|
74
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
75
|
+
return "npm";
|
|
76
|
+
}
|
|
77
|
+
function installCommand(pm) {
|
|
78
|
+
switch (pm) {
|
|
79
|
+
case "pnpm":
|
|
80
|
+
return "pnpm add costly";
|
|
81
|
+
case "yarn":
|
|
82
|
+
return "yarn add costly";
|
|
83
|
+
case "bun":
|
|
84
|
+
return "bun add costly";
|
|
85
|
+
default:
|
|
86
|
+
return "npm install costly";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"]);
|
|
90
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
91
|
+
"node_modules",
|
|
92
|
+
".next",
|
|
93
|
+
"dist",
|
|
94
|
+
"build",
|
|
95
|
+
".git",
|
|
96
|
+
".vercel",
|
|
97
|
+
"coverage"
|
|
98
|
+
]);
|
|
99
|
+
function scanDirectory(dir, cwd) {
|
|
100
|
+
const results = [];
|
|
101
|
+
let entries;
|
|
102
|
+
try {
|
|
103
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
104
|
+
} catch {
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
109
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
110
|
+
const fullPath = path.join(dir, entry.name);
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
results.push(...scanDirectory(fullPath, cwd));
|
|
113
|
+
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
114
|
+
const found = scanFile(fullPath, cwd);
|
|
115
|
+
if (found.length > 0) results.push(...found);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
function scanFile(filePath, cwd) {
|
|
121
|
+
const results = [];
|
|
122
|
+
let content;
|
|
123
|
+
try {
|
|
124
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
125
|
+
} catch {
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
if (content.includes('from "costly"') || content.includes("from 'costly'") || content.includes('require("costly")') || content.includes("require('costly')")) {
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
const pattern = /(?:(?:export\s+)?(?:const|let|var)\s+)(\w+)\s*=\s*(new\s+Anthropic\s*\([^)]*\))/;
|
|
133
|
+
for (let i = 0; i < lines.length; i++) {
|
|
134
|
+
const match = lines[i].match(pattern);
|
|
135
|
+
if (match) {
|
|
136
|
+
results.push({
|
|
137
|
+
filePath,
|
|
138
|
+
relativePath: path.relative(cwd, filePath),
|
|
139
|
+
line: i + 1,
|
|
140
|
+
match: lines[i].trim(),
|
|
141
|
+
varName: match[1],
|
|
142
|
+
fullInit: match[2]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
function applyChanges(found) {
|
|
149
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const f of found) {
|
|
151
|
+
const existing = byFile.get(f.filePath) || [];
|
|
152
|
+
existing.push(f);
|
|
153
|
+
byFile.set(f.filePath, existing);
|
|
154
|
+
}
|
|
155
|
+
for (const [filePath, items] of byFile) {
|
|
156
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
157
|
+
const hasCostlyImport = content.includes('from "costly"') || content.includes("from 'costly'") || content.includes('require("costly")') || content.includes("require('costly')");
|
|
158
|
+
const usesEsm = content.includes("import ") && (content.includes(" from ") || content.includes("import {"));
|
|
159
|
+
if (!hasCostlyImport) {
|
|
160
|
+
const importStatement = usesEsm ? 'import { costly } from "costly";\n' : 'const { costly } = require("costly");\n';
|
|
161
|
+
const lines = content.split("\n");
|
|
162
|
+
let insertIndex = 0;
|
|
163
|
+
for (let i = 0; i < lines.length; i++) {
|
|
164
|
+
const line = lines[i].trim();
|
|
165
|
+
if (line.startsWith("import ") || line.startsWith("import{") || line.includes("require(") && (line.startsWith("const ") || line.startsWith("let ") || line.startsWith("var "))) {
|
|
166
|
+
insertIndex = i + 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
lines.splice(insertIndex, 0, importStatement.trimEnd());
|
|
170
|
+
content = lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
for (const item of items) {
|
|
173
|
+
content = content.replace(item.fullInit, `costly().wrap(${item.fullInit})`);
|
|
174
|
+
}
|
|
175
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function addToEnvFile(cwd, apiKey) {
|
|
179
|
+
const envPath = path.join(cwd, ".env");
|
|
180
|
+
let content = "";
|
|
181
|
+
if (fs.existsSync(envPath)) {
|
|
182
|
+
content = fs.readFileSync(envPath, "utf-8");
|
|
183
|
+
if (content.includes("COSTLY_API_KEY=")) {
|
|
184
|
+
content = content.replace(/COSTLY_API_KEY=.*/, `COSTLY_API_KEY=${apiKey}`);
|
|
185
|
+
fs.writeFileSync(envPath, content, "utf-8");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const newLine = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
190
|
+
fs.writeFileSync(envPath, content + newLine + `COSTLY_API_KEY=${apiKey}
|
|
191
|
+
`, "utf-8");
|
|
192
|
+
}
|
|
193
|
+
async function verifyApiKey(apiKey) {
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(VERIFY_URL, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
Authorization: `Bearer ${apiKey}`
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return res.ok;
|
|
203
|
+
} catch {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function installPackage(pm) {
|
|
208
|
+
const { execSync } = await import("child_process");
|
|
209
|
+
const cmd = installCommand(pm);
|
|
210
|
+
try {
|
|
211
|
+
execSync(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
212
|
+
success(`Installed costly ${dim(`via ${pm}`)}`);
|
|
213
|
+
} catch {
|
|
214
|
+
warn(`Could not auto-install. Run manually: ${cmd}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function main() {
|
|
218
|
+
const cwd = process.cwd();
|
|
219
|
+
log("");
|
|
220
|
+
log(` ${BOLD}costly${RESET} ${dim(`v${VERSION}`)}`);
|
|
221
|
+
log("");
|
|
222
|
+
const apiKey = await ask(
|
|
223
|
+
` ${CYAN}?${RESET} Paste your API key ${dim("(from getcostly.dev/dashboard)")}: `
|
|
224
|
+
);
|
|
225
|
+
if (!apiKey || !apiKey.startsWith("ck_")) {
|
|
226
|
+
log("");
|
|
227
|
+
error("Invalid API key. It should start with ck_");
|
|
228
|
+
log(` Get yours at ${CYAN}https://getcostly.dev/dashboard${RESET}`);
|
|
229
|
+
log("");
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
const valid = await verifyApiKey(apiKey);
|
|
233
|
+
if (!valid) {
|
|
234
|
+
log("");
|
|
235
|
+
error("API key not recognized. Check your key and try again.");
|
|
236
|
+
log(` Get yours at ${CYAN}https://getcostly.dev/dashboard${RESET}`);
|
|
237
|
+
log("");
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
success("API key verified");
|
|
241
|
+
log("");
|
|
242
|
+
log(` Scanning for Anthropic SDK usage...`);
|
|
243
|
+
log("");
|
|
244
|
+
const found = scanDirectory(cwd, cwd);
|
|
245
|
+
if (found.length === 0) {
|
|
246
|
+
warn("No `new Anthropic()` calls found in your codebase.");
|
|
247
|
+
log("");
|
|
248
|
+
log(` You can add Costly manually:`);
|
|
249
|
+
log("");
|
|
250
|
+
log(` ${DIM}import { costly } from "costly";${RESET}`);
|
|
251
|
+
log(` ${DIM}const client = costly().wrap(new Anthropic());${RESET}`);
|
|
252
|
+
log("");
|
|
253
|
+
const shouldContinue = await confirm(
|
|
254
|
+
` Install costly and add API key to .env anyway?`
|
|
255
|
+
);
|
|
256
|
+
if (shouldContinue) {
|
|
257
|
+
const pm2 = detectPackageManager(cwd);
|
|
258
|
+
log("");
|
|
259
|
+
await installPackage(pm2);
|
|
260
|
+
addToEnvFile(cwd, apiKey);
|
|
261
|
+
success("Added COSTLY_API_KEY to .env");
|
|
262
|
+
log("");
|
|
263
|
+
log(` Add the wrapper to your code when you're ready.`);
|
|
264
|
+
}
|
|
265
|
+
log("");
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
268
|
+
log(
|
|
269
|
+
` Found ${found.length} file${found.length === 1 ? "" : "s"} using the Anthropic SDK:`
|
|
270
|
+
);
|
|
271
|
+
log("");
|
|
272
|
+
for (const f of found) {
|
|
273
|
+
log(` ${f.relativePath}:${f.line}`);
|
|
274
|
+
log(` ${dim(f.match)}`);
|
|
275
|
+
log("");
|
|
276
|
+
}
|
|
277
|
+
const shouldWrap = await confirm(
|
|
278
|
+
` Wrap ${found.length === 1 ? "this file" : "these files"} with Costly?`
|
|
279
|
+
);
|
|
280
|
+
if (!shouldWrap) {
|
|
281
|
+
log("");
|
|
282
|
+
log(` No worries. You can add Costly manually:`);
|
|
283
|
+
log(` ${DIM}const client = costly().wrap(new Anthropic());${RESET}`);
|
|
284
|
+
log("");
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
log("");
|
|
288
|
+
log(` Here's what will change:`);
|
|
289
|
+
log("");
|
|
290
|
+
for (const f of found) {
|
|
291
|
+
log(` ${BOLD}${f.relativePath}${RESET}`);
|
|
292
|
+
log(` ${"\u2500".repeat(40)}`);
|
|
293
|
+
log(` ${GREEN}+${RESET} import { costly } from "costly";`);
|
|
294
|
+
log(` ${RED}-${RESET} ${f.match}`);
|
|
295
|
+
const wrappedInit = `costly().wrap(${f.fullInit})`;
|
|
296
|
+
const newLine = f.match.replace(f.fullInit, wrappedInit);
|
|
297
|
+
log(` ${GREEN}+${RESET} ${newLine}`);
|
|
298
|
+
log("");
|
|
299
|
+
}
|
|
300
|
+
const shouldApply = await confirm(` Apply changes?`);
|
|
301
|
+
if (!shouldApply) {
|
|
302
|
+
log("");
|
|
303
|
+
log(` Cancelled. No files were modified.`);
|
|
304
|
+
log("");
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
log("");
|
|
308
|
+
const pm = detectPackageManager(cwd);
|
|
309
|
+
await installPackage(pm);
|
|
310
|
+
applyChanges(found);
|
|
311
|
+
for (const f of found) {
|
|
312
|
+
success(`Updated ${f.relativePath}`);
|
|
313
|
+
}
|
|
314
|
+
addToEnvFile(cwd, apiKey);
|
|
315
|
+
success("Added COSTLY_API_KEY to .env");
|
|
316
|
+
log("");
|
|
317
|
+
log(
|
|
318
|
+
` ${GREEN}${BOLD}You're all set.${RESET} Your dashboard will light up within 48 hours.`
|
|
319
|
+
);
|
|
320
|
+
log(` ${dim("https://getcostly.dev/dashboard")}`);
|
|
321
|
+
log("");
|
|
322
|
+
}
|
|
323
|
+
function showHelp() {
|
|
324
|
+
log("");
|
|
325
|
+
log(` ${BOLD}costly${RESET} ${dim(`v${VERSION}`)}`);
|
|
326
|
+
log("");
|
|
327
|
+
log(` ${BOLD}Usage:${RESET}`);
|
|
328
|
+
log(` costly init Set up Costly in your project`);
|
|
329
|
+
log("");
|
|
330
|
+
log(` ${BOLD}Learn more:${RESET}`);
|
|
331
|
+
log(` ${CYAN}https://getcostly.dev${RESET}`);
|
|
332
|
+
log("");
|
|
333
|
+
}
|
|
334
|
+
var command = process.argv[2];
|
|
335
|
+
if (command === "init") {
|
|
336
|
+
main().catch((err) => {
|
|
337
|
+
error(err.message || "An unexpected error occurred");
|
|
338
|
+
process.exit(1);
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
showHelp();
|
|
342
|
+
}
|
package/dist/cli.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ var RED = "\x1B[31m";
|
|
|
11
11
|
var YELLOW = "\x1B[33m";
|
|
12
12
|
var RESET = "\x1B[0m";
|
|
13
13
|
var BOLD = "\x1B[1m";
|
|
14
|
+
var VERSION = "0.1.1";
|
|
14
15
|
var VERIFY_URL = "https://www.getcostly.dev/api/v1/verify-key";
|
|
15
16
|
function log(msg) {
|
|
16
17
|
console.log(msg);
|
|
@@ -45,7 +46,8 @@ async function confirm(question) {
|
|
|
45
46
|
}
|
|
46
47
|
function detectPackageManager(cwd) {
|
|
47
48
|
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
48
|
-
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock")))
|
|
49
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock")))
|
|
50
|
+
return "bun";
|
|
49
51
|
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
50
52
|
return "npm";
|
|
51
53
|
}
|
|
@@ -62,7 +64,15 @@ function installCommand(pm) {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"]);
|
|
65
|
-
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
67
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
68
|
+
"node_modules",
|
|
69
|
+
".next",
|
|
70
|
+
"dist",
|
|
71
|
+
"build",
|
|
72
|
+
".git",
|
|
73
|
+
".vercel",
|
|
74
|
+
"coverage"
|
|
75
|
+
]);
|
|
66
76
|
function scanDirectory(dir, cwd) {
|
|
67
77
|
const results = [];
|
|
68
78
|
let entries;
|
|
@@ -137,10 +147,7 @@ function applyChanges(found) {
|
|
|
137
147
|
content = lines.join("\n");
|
|
138
148
|
}
|
|
139
149
|
for (const item of items) {
|
|
140
|
-
content = content.replace(
|
|
141
|
-
item.fullInit,
|
|
142
|
-
`costly().wrap(${item.fullInit})`
|
|
143
|
-
);
|
|
150
|
+
content = content.replace(item.fullInit, `costly().wrap(${item.fullInit})`);
|
|
144
151
|
}
|
|
145
152
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
146
153
|
}
|
|
@@ -174,12 +181,24 @@ async function verifyApiKey(apiKey) {
|
|
|
174
181
|
return true;
|
|
175
182
|
}
|
|
176
183
|
}
|
|
184
|
+
async function installPackage(pm) {
|
|
185
|
+
const { execSync } = await import("child_process");
|
|
186
|
+
const cmd = installCommand(pm);
|
|
187
|
+
try {
|
|
188
|
+
execSync(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
189
|
+
success(`Installed costly ${dim(`via ${pm}`)}`);
|
|
190
|
+
} catch {
|
|
191
|
+
warn(`Could not auto-install. Run manually: ${cmd}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
177
194
|
async function main() {
|
|
178
195
|
const cwd = process.cwd();
|
|
179
196
|
log("");
|
|
180
|
-
log(` ${BOLD}costly${RESET} ${dim(
|
|
197
|
+
log(` ${BOLD}costly${RESET} ${dim(`v${VERSION}`)}`);
|
|
181
198
|
log("");
|
|
182
|
-
const apiKey = await ask(
|
|
199
|
+
const apiKey = await ask(
|
|
200
|
+
` ${CYAN}?${RESET} Paste your API key ${dim("(from getcostly.dev/dashboard)")}: `
|
|
201
|
+
);
|
|
183
202
|
if (!apiKey || !apiKey.startsWith("ck_")) {
|
|
184
203
|
log("");
|
|
185
204
|
error("Invalid API key. It should start with ck_");
|
|
@@ -208,7 +227,9 @@ async function main() {
|
|
|
208
227
|
log(` ${DIM}import { costly } from "costly";${RESET}`);
|
|
209
228
|
log(` ${DIM}const client = costly().wrap(new Anthropic());${RESET}`);
|
|
210
229
|
log("");
|
|
211
|
-
const shouldContinue = await confirm(
|
|
230
|
+
const shouldContinue = await confirm(
|
|
231
|
+
` Install costly and add API key to .env anyway?`
|
|
232
|
+
);
|
|
212
233
|
if (shouldContinue) {
|
|
213
234
|
const pm2 = detectPackageManager(cwd);
|
|
214
235
|
log("");
|
|
@@ -221,14 +242,18 @@ async function main() {
|
|
|
221
242
|
log("");
|
|
222
243
|
process.exit(0);
|
|
223
244
|
}
|
|
224
|
-
log(
|
|
245
|
+
log(
|
|
246
|
+
` Found ${found.length} file${found.length === 1 ? "" : "s"} using the Anthropic SDK:`
|
|
247
|
+
);
|
|
225
248
|
log("");
|
|
226
249
|
for (const f of found) {
|
|
227
250
|
log(` ${f.relativePath}:${f.line}`);
|
|
228
251
|
log(` ${dim(f.match)}`);
|
|
229
252
|
log("");
|
|
230
253
|
}
|
|
231
|
-
const shouldWrap = await confirm(
|
|
254
|
+
const shouldWrap = await confirm(
|
|
255
|
+
` Wrap ${found.length === 1 ? "this file" : "these files"} with Costly?`
|
|
256
|
+
);
|
|
232
257
|
if (!shouldWrap) {
|
|
233
258
|
log("");
|
|
234
259
|
log(` No worries. You can add Costly manually:`);
|
|
@@ -266,23 +291,15 @@ async function main() {
|
|
|
266
291
|
addToEnvFile(cwd, apiKey);
|
|
267
292
|
success("Added COSTLY_API_KEY to .env");
|
|
268
293
|
log("");
|
|
269
|
-
log(
|
|
294
|
+
log(
|
|
295
|
+
` ${GREEN}${BOLD}You're all set.${RESET} Your dashboard will light up within 48 hours.`
|
|
296
|
+
);
|
|
270
297
|
log(` ${dim("https://getcostly.dev/dashboard")}`);
|
|
271
298
|
log("");
|
|
272
299
|
}
|
|
273
|
-
async function installPackage(pm) {
|
|
274
|
-
const { execSync } = await import("child_process");
|
|
275
|
-
const cmd = installCommand(pm);
|
|
276
|
-
try {
|
|
277
|
-
execSync(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
278
|
-
success(`Installed costly ${dim(`via ${pm}`)}`);
|
|
279
|
-
} catch {
|
|
280
|
-
warn(`Could not auto-install. Run manually: ${cmd}`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
300
|
function showHelp() {
|
|
284
301
|
log("");
|
|
285
|
-
log(` ${BOLD}costly${RESET} ${dim(
|
|
302
|
+
log(` ${BOLD}costly${RESET} ${dim(`v${VERSION}`)}`);
|
|
286
303
|
log("");
|
|
287
304
|
log(` ${BOLD}Usage:${RESET}`);
|
|
288
305
|
log(` costly init Set up Costly in your project`);
|
package/dist/index.cjs
CHANGED
|
@@ -33,7 +33,7 @@ var LogBatcher = class {
|
|
|
33
33
|
this.endpoint = opts.endpoint;
|
|
34
34
|
this.apiKey = opts.apiKey;
|
|
35
35
|
this.flushInterval = opts.flushInterval;
|
|
36
|
-
this.
|
|
36
|
+
this.batchSize = opts.batchSize;
|
|
37
37
|
this.debug = opts.debug;
|
|
38
38
|
if (typeof process !== "undefined") {
|
|
39
39
|
process.on("beforeExit", () => {
|
|
@@ -43,7 +43,7 @@ var LogBatcher = class {
|
|
|
43
43
|
}
|
|
44
44
|
add(log) {
|
|
45
45
|
this.queue.push(log);
|
|
46
|
-
if (this.queue.length >= this.
|
|
46
|
+
if (this.queue.length >= this.batchSize) {
|
|
47
47
|
this.flush();
|
|
48
48
|
} else if (!this.timer) {
|
|
49
49
|
this.timer = setTimeout(() => this.flush(), this.flushInterval);
|
|
@@ -166,7 +166,7 @@ function getCallSite() {
|
|
|
166
166
|
// src/index.ts
|
|
167
167
|
var DEFAULT_ENDPOINT = "https://www.getcostly.dev/api/v1/ingest";
|
|
168
168
|
var DEFAULT_FLUSH_INTERVAL = 5e3;
|
|
169
|
-
var
|
|
169
|
+
var DEFAULT_BATCH_SIZE = 10;
|
|
170
170
|
var INTERCEPTED_METHODS = /* @__PURE__ */ new Set(["create", "stream"]);
|
|
171
171
|
function costly(config) {
|
|
172
172
|
const apiKey = config?.apiKey ?? process.env.COSTLY_API_KEY;
|
|
@@ -178,7 +178,7 @@ function costly(config) {
|
|
|
178
178
|
endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,
|
|
179
179
|
apiKey,
|
|
180
180
|
flushInterval: config?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
|
|
181
|
-
|
|
181
|
+
batchSize: config?.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
182
182
|
debug: config?.debug ?? false
|
|
183
183
|
});
|
|
184
184
|
return {
|
|
@@ -332,4 +332,3 @@ function wrapStream(stream, buildLog, batcher) {
|
|
|
332
332
|
0 && (module.exports = {
|
|
333
333
|
costly
|
|
334
334
|
});
|
|
335
|
-
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var LogBatcher = class {
|
|
|
7
7
|
this.endpoint = opts.endpoint;
|
|
8
8
|
this.apiKey = opts.apiKey;
|
|
9
9
|
this.flushInterval = opts.flushInterval;
|
|
10
|
-
this.
|
|
10
|
+
this.batchSize = opts.batchSize;
|
|
11
11
|
this.debug = opts.debug;
|
|
12
12
|
if (typeof process !== "undefined") {
|
|
13
13
|
process.on("beforeExit", () => {
|
|
@@ -17,7 +17,7 @@ var LogBatcher = class {
|
|
|
17
17
|
}
|
|
18
18
|
add(log) {
|
|
19
19
|
this.queue.push(log);
|
|
20
|
-
if (this.queue.length >= this.
|
|
20
|
+
if (this.queue.length >= this.batchSize) {
|
|
21
21
|
this.flush();
|
|
22
22
|
} else if (!this.timer) {
|
|
23
23
|
this.timer = setTimeout(() => this.flush(), this.flushInterval);
|
|
@@ -140,7 +140,7 @@ function getCallSite() {
|
|
|
140
140
|
// src/index.ts
|
|
141
141
|
var DEFAULT_ENDPOINT = "https://www.getcostly.dev/api/v1/ingest";
|
|
142
142
|
var DEFAULT_FLUSH_INTERVAL = 5e3;
|
|
143
|
-
var
|
|
143
|
+
var DEFAULT_BATCH_SIZE = 10;
|
|
144
144
|
var INTERCEPTED_METHODS = /* @__PURE__ */ new Set(["create", "stream"]);
|
|
145
145
|
function costly(config) {
|
|
146
146
|
const apiKey = config?.apiKey ?? process.env.COSTLY_API_KEY;
|
|
@@ -152,7 +152,7 @@ function costly(config) {
|
|
|
152
152
|
endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,
|
|
153
153
|
apiKey,
|
|
154
154
|
flushInterval: config?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
|
|
155
|
-
|
|
155
|
+
batchSize: config?.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
156
156
|
debug: config?.debug ?? false
|
|
157
157
|
});
|
|
158
158
|
return {
|
|
@@ -305,4 +305,3 @@ function wrapStream(stream, buildLog, batcher) {
|
|
|
305
305
|
export {
|
|
306
306
|
costly
|
|
307
307
|
};
|
|
308
|
-
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "costly",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "AI cost waste detector — lightweight wrapper for the Anthropic SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -40,13 +40,14 @@
|
|
|
40
40
|
"typescript": "^5.7.0",
|
|
41
41
|
"vitest": "^4.0.18"
|
|
42
42
|
},
|
|
43
|
+
"author": "Costly <hello@getcostly.dev>",
|
|
43
44
|
"license": "MIT",
|
|
44
45
|
"engines": {
|
|
45
46
|
"node": ">=18"
|
|
46
47
|
},
|
|
47
48
|
"repository": {
|
|
48
49
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/
|
|
50
|
+
"url": "https://github.com/itsdannyt/costly"
|
|
50
51
|
},
|
|
51
52
|
"keywords": [
|
|
52
53
|
"anthropic",
|
package/dist/index.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/batcher.ts","../src/pricing.ts","../src/hash.ts","../src/callsite.ts"],"sourcesContent":["import type { CostlyConfig, CostlyCallMetadata, RequestLog } from \"./types.js\";\nimport { LogBatcher } from \"./batcher.js\";\nimport { calculateCost } from \"./pricing.js\";\nimport { hashString } from \"./hash.js\";\nimport { getCallSite } from \"./callsite.js\";\n\nexport type { CostlyConfig, CostlyCallMetadata, RequestLog };\n\nconst DEFAULT_ENDPOINT = \"https://www.getcostly.dev/api/v1/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 5000; // 5 seconds\nconst DEFAULT_FLUSH_BATCH_SIZE = 10;\n\n// Methods to intercept on the Anthropic SDK\nconst INTERCEPTED_METHODS = new Set([\"create\", \"stream\"]);\n\ninterface CostlyClient {\n wrap<T extends object>(client: T): T;\n flush(): Promise<void>;\n shutdown(): Promise<void>;\n}\n\nexport function costly(config?: Partial<CostlyConfig>): CostlyClient {\n const apiKey = config?.apiKey ?? process.env.COSTLY_API_KEY;\n const projectId = config?.projectId ?? process.env.COSTLY_PROJECT_ID ?? \"_\";\n\n if (!apiKey) {\n throw new Error(\"[costly] apiKey is required — pass it in config or set COSTLY_API_KEY env var\");\n }\n\n const batcher = new LogBatcher({\n endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,\n apiKey,\n flushInterval: config?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,\n flushBatchSize: config?.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,\n debug: config?.debug ?? false,\n });\n\n return {\n wrap<T extends object>(client: T): T {\n return wrapClient(client, projectId, batcher);\n },\n flush() {\n return batcher.flushAsync();\n },\n shutdown() {\n return batcher.shutdown();\n },\n };\n}\n\n// Cache proxies to avoid creating a new one on every property access\nconst proxyCache = new WeakMap<object, object>();\n\nfunction wrapClient<T extends object>(\n client: T,\n projectId: string,\n batcher: LogBatcher,\n): T {\n const cached = proxyCache.get(client);\n if (cached) return cached as T;\n\n const proxy = new Proxy(client, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n\n // Wrap intercepted methods (create, stream)\n if (INTERCEPTED_METHODS.has(prop as string) && typeof value === \"function\") {\n return wrapMethod(value.bind(target), projectId, batcher);\n }\n\n // Recursively wrap nested objects (e.g., anthropic.messages)\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return wrapClient(value as object, projectId, batcher);\n }\n\n return value;\n },\n });\n\n proxyCache.set(client, proxy);\n return proxy as T;\n}\n\nfunction wrapMethod(\n originalMethod: (...args: unknown[]) => unknown,\n projectId: string,\n batcher: LogBatcher,\n): (...args: unknown[]) => unknown {\n return function costlyWrappedMethod(...args: unknown[]): unknown {\n const callSite = getCallSite();\n const startTime = Date.now();\n\n // Extract and strip costly metadata from the request params\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const costlyMeta = params.costly as CostlyCallMetadata | undefined;\n\n // Create a clean copy without the costly property\n if (costlyMeta) {\n const { costly: _, ...cleanParams } = params;\n args[0] = cleanParams;\n }\n\n // Extract data we need for logging before the call\n const model = (params.model as string) ?? \"unknown\";\n const maxTokens = (params.max_tokens as number) ?? null;\n const messages = params.messages as Array<{ role: string; content: unknown }> | undefined;\n const systemPrompt = params.system;\n\n // Build prompt hash from messages content\n const promptContent = messages\n ? JSON.stringify(messages.map((m) => ({ role: m.role, content: m.content })))\n : \"\";\n const promptHash = hashString(promptContent);\n const systemPromptHash = systemPrompt\n ? hashString(typeof systemPrompt === \"string\" ? systemPrompt : JSON.stringify(systemPrompt))\n : null;\n\n const tag = costlyMeta?.tag ?? null;\n const userId = costlyMeta?.userId ?? null;\n const autoCallSite = tag ? null : callSite;\n\n function buildLog(\n inputTokens: number,\n outputTokens: number,\n status: \"success\" | \"error\",\n errorType: string | null,\n ): RequestLog {\n return {\n projectId,\n timestamp: new Date().toISOString(),\n model,\n tag,\n userId,\n inputTokens,\n outputTokens,\n totalCost: calculateCost(model, inputTokens, outputTokens),\n maxTokens,\n status,\n errorType,\n promptHash,\n systemPromptHash,\n callSite: autoCallSite,\n durationMs: Date.now() - startTime,\n };\n }\n\n let result: unknown;\n try {\n result = originalMethod(...args);\n } catch (err) {\n // Synchronous error — log it but rethrow\n const error = err as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(0, 0, \"error\", error.error?.type ?? error.message ?? \"unknown_error\"));\n throw err;\n }\n\n // Handle thenable results (Promises, Streams with .finalMessage(), etc.)\n if (result && typeof (result as { then?: unknown }).then === \"function\") {\n (result as Promise<unknown>).then(\n (response) => {\n try {\n const res = response as Record<string, unknown>;\n const usage = res.usage as { input_tokens?: number; output_tokens?: number } | undefined;\n batcher.add(buildLog(usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, \"success\", null));\n } catch {\n // Never crash user code from logging\n }\n },\n (error) => {\n try {\n const err = error as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(0, 0, \"error\", err.error?.type ?? err.message ?? \"unknown_error\"));\n } catch {\n // Never crash user code from logging\n }\n },\n );\n }\n\n // If result is a streaming object (has Symbol.asyncIterator), wrap it to capture usage\n if (result && typeof (result as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === \"function\") {\n return wrapStream(result as AsyncIterable<unknown>, buildLog, batcher);\n }\n\n return result;\n };\n}\n\nfunction wrapStream(\n stream: AsyncIterable<unknown>,\n buildLog: (input: number, output: number, status: \"success\" | \"error\", errorType: string | null) => RequestLog,\n batcher: LogBatcher,\n): AsyncIterable<unknown> {\n const originalIterator = stream[Symbol.asyncIterator]();\n\n // Preserve all properties of the original stream object (e.g., .finalMessage(), .controller, etc.)\n const wrappedStream = Object.create(stream);\n\n wrappedStream[Symbol.asyncIterator] = () => {\n let inputTokens = 0;\n let outputTokens = 0;\n\n return {\n async next(): Promise<IteratorResult<unknown>> {\n try {\n const result = await originalIterator.next();\n\n if (result.done) {\n // Stream ended — log total usage\n batcher.add(buildLog(inputTokens, outputTokens, \"success\", null));\n return result;\n }\n\n // Accumulate usage from stream events\n const event = result.value as Record<string, unknown>;\n if (event.type === \"message_delta\") {\n const usage = event.usage as { output_tokens?: number } | undefined;\n if (usage?.output_tokens) {\n outputTokens = usage.output_tokens;\n }\n } else if (event.type === \"message_start\") {\n const message = event.message as { usage?: { input_tokens?: number } } | undefined;\n if (message?.usage?.input_tokens) {\n inputTokens = message.usage.input_tokens;\n }\n }\n\n return result;\n } catch (err) {\n const error = err as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(inputTokens, outputTokens, \"error\", error.error?.type ?? error.message ?? \"unknown_error\"));\n throw err;\n }\n },\n async return(value?: unknown): Promise<IteratorResult<unknown>> {\n // Stream was cancelled early — still log what we have\n batcher.add(buildLog(inputTokens, outputTokens, \"success\", null));\n if (originalIterator.return) {\n return originalIterator.return(value);\n }\n return { done: true, value: undefined };\n },\n };\n };\n\n return wrappedStream;\n}\n","import type { RequestLog } from \"./types.js\";\n\nexport class LogBatcher {\n private queue: RequestLog[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private pendingSends: Promise<void>[] = [];\n private endpoint: string;\n private apiKey: string;\n private flushInterval: number;\n private flushBatchSize: number;\n private debug: boolean;\n\n constructor(opts: {\n endpoint: string;\n apiKey: string;\n flushInterval: number;\n flushBatchSize: number;\n debug: boolean;\n }) {\n this.endpoint = opts.endpoint;\n this.apiKey = opts.apiKey;\n this.flushInterval = opts.flushInterval;\n this.flushBatchSize = opts.flushBatchSize;\n this.debug = opts.debug;\n\n if (typeof process !== \"undefined\") {\n process.on(\"beforeExit\", () => {\n this.flush();\n });\n }\n }\n\n add(log: RequestLog): void {\n this.queue.push(log);\n\n if (this.queue.length >= this.flushBatchSize) {\n this.flush();\n } else if (!this.timer) {\n this.timer = setTimeout(() => this.flush(), this.flushInterval);\n // Don't keep the Node process alive just for logging\n if (this.timer && typeof this.timer === \"object\" && \"unref\" in this.timer) {\n this.timer.unref();\n }\n }\n }\n\n flush(): void {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n\n if (this.queue.length === 0) return;\n\n const batch = this.queue.splice(0);\n\n const sendPromise = this.send(batch).catch((err) => {\n if (this.debug) {\n console.warn(\"[costly] Failed to send logs:\", err.message);\n }\n });\n\n this.pendingSends.push(sendPromise);\n sendPromise.finally(() => {\n const idx = this.pendingSends.indexOf(sendPromise);\n if (idx !== -1) this.pendingSends.splice(idx, 1);\n });\n }\n\n async flushAsync(): Promise<void> {\n this.flush();\n await Promise.all(this.pendingSends);\n }\n\n async shutdown(): Promise<void> {\n await this.flushAsync();\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n private async send(batch: RequestLog[]): Promise<void> {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({ logs: batch }),\n });\n\n if (!res.ok && this.debug) {\n console.warn(`[costly] Ingest responded ${res.status}: ${res.statusText}`);\n }\n }\n}\n","// Anthropic pricing per million tokens (as of March 2026)\n// Source: https://docs.anthropic.com/en/docs/about-claude/models\n\ninterface ModelPricing {\n inputPerMillion: number;\n outputPerMillion: number;\n}\n\nconst PRICING: Record<string, ModelPricing> = {\n // Claude 4.x family\n \"claude-opus-4-20250514\": { inputPerMillion: 15, outputPerMillion: 75 },\n \"claude-sonnet-4-20250514\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-haiku-4-5-20251001\": { inputPerMillion: 0.8, outputPerMillion: 4 },\n\n // Claude 3.5 family (deprecated but still in use)\n \"claude-3-5-sonnet-20241022\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-5-sonnet-20240620\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-5-haiku-20241022\": { inputPerMillion: 0.8, outputPerMillion: 4 },\n\n // Claude 3 family (deprecated)\n \"claude-3-opus-20240229\": { inputPerMillion: 15, outputPerMillion: 75 },\n \"claude-3-sonnet-20240229\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-haiku-20240307\": { inputPerMillion: 0.25, outputPerMillion: 1.25 },\n};\n\n// Model alias resolution\nconst ALIASES: Record<string, string> = {\n \"claude-opus-4-0\": \"claude-opus-4-20250514\",\n \"claude-sonnet-4-0\": \"claude-sonnet-4-20250514\",\n \"claude-haiku-4-5-latest\": \"claude-haiku-4-5-20251001\",\n \"claude-3-5-sonnet-latest\": \"claude-3-5-sonnet-20241022\",\n \"claude-3-5-haiku-latest\": \"claude-3-5-haiku-20241022\",\n \"claude-3-opus-latest\": \"claude-3-opus-20240229\",\n \"claude-3-sonnet-latest\": \"claude-3-sonnet-20240229\",\n \"claude-3-haiku-latest\": \"claude-3-haiku-20240307\",\n};\n\nexport function calculateCost(\n model: string,\n inputTokens: number,\n outputTokens: number,\n): number {\n const resolvedModel = ALIASES[model] || model;\n const pricing = PRICING[resolvedModel];\n\n if (!pricing) {\n // Unknown model — return 0 rather than crashing the user's app\n return 0;\n }\n\n const inputCost = (inputTokens / 1_000_000) * pricing.inputPerMillion;\n const outputCost = (outputTokens / 1_000_000) * pricing.outputPerMillion;\n\n return Math.round((inputCost + outputCost) * 1_000_000) / 1_000_000; // 6 decimal places\n}\n","// Simple fast hash for prompt deduplication\n// Uses djb2 algorithm — not cryptographic, just for grouping identical prompts\n\nexport function hashString(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff;\n }\n return (hash >>> 0).toString(36);\n}\n","// Extract the caller's file + line number from the stack trace\n// Used for auto-tagging when no manual tag is provided\n\nexport function getCallSite(): string | null {\n const err = new Error();\n const stack = err.stack;\n if (!stack) return null;\n\n const lines = stack.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip the Error line itself\n if (trimmed.startsWith(\"Error\")) continue;\n\n // Skip costly SDK internals by matching on our known file names\n if (\n trimmed.includes(\"node_modules/costly/\") ||\n trimmed.includes(\"costlyWrappedMethod\") ||\n trimmed.includes(\"wrapMethod\") ||\n trimmed.includes(\"wrapClient\") ||\n trimmed.includes(\"wrapStream\") ||\n trimmed.includes(\"callsite.\") ||\n trimmed.includes(\"batcher.\") ||\n trimmed.includes(\"LogBatcher\")\n ) {\n continue;\n }\n\n // Extract file:line from the stack frame\n const match = trimmed.match(/\\((.+):(\\d+):\\d+\\)/) ||\n trimmed.match(/at (.+):(\\d+):\\d+/);\n\n if (match) {\n const file = match[1].replace(/^.*[/\\\\]/, \"\"); // basename only\n const line = match[2];\n return `${file}:${line}`;\n }\n }\n\n return null;\n}\n"],"mappings":"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","names":["line"]}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/batcher.ts","../src/pricing.ts","../src/hash.ts","../src/callsite.ts","../src/index.ts"],"sourcesContent":["import type { RequestLog } from \"./types.js\";\n\nexport class LogBatcher {\n private queue: RequestLog[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private pendingSends: Promise<void>[] = [];\n private endpoint: string;\n private apiKey: string;\n private flushInterval: number;\n private flushBatchSize: number;\n private debug: boolean;\n\n constructor(opts: {\n endpoint: string;\n apiKey: string;\n flushInterval: number;\n flushBatchSize: number;\n debug: boolean;\n }) {\n this.endpoint = opts.endpoint;\n this.apiKey = opts.apiKey;\n this.flushInterval = opts.flushInterval;\n this.flushBatchSize = opts.flushBatchSize;\n this.debug = opts.debug;\n\n if (typeof process !== \"undefined\") {\n process.on(\"beforeExit\", () => {\n this.flush();\n });\n }\n }\n\n add(log: RequestLog): void {\n this.queue.push(log);\n\n if (this.queue.length >= this.flushBatchSize) {\n this.flush();\n } else if (!this.timer) {\n this.timer = setTimeout(() => this.flush(), this.flushInterval);\n // Don't keep the Node process alive just for logging\n if (this.timer && typeof this.timer === \"object\" && \"unref\" in this.timer) {\n this.timer.unref();\n }\n }\n }\n\n flush(): void {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n\n if (this.queue.length === 0) return;\n\n const batch = this.queue.splice(0);\n\n const sendPromise = this.send(batch).catch((err) => {\n if (this.debug) {\n console.warn(\"[costly] Failed to send logs:\", err.message);\n }\n });\n\n this.pendingSends.push(sendPromise);\n sendPromise.finally(() => {\n const idx = this.pendingSends.indexOf(sendPromise);\n if (idx !== -1) this.pendingSends.splice(idx, 1);\n });\n }\n\n async flushAsync(): Promise<void> {\n this.flush();\n await Promise.all(this.pendingSends);\n }\n\n async shutdown(): Promise<void> {\n await this.flushAsync();\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n }\n\n private async send(batch: RequestLog[]): Promise<void> {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({ logs: batch }),\n });\n\n if (!res.ok && this.debug) {\n console.warn(`[costly] Ingest responded ${res.status}: ${res.statusText}`);\n }\n }\n}\n","// Anthropic pricing per million tokens (as of March 2026)\n// Source: https://docs.anthropic.com/en/docs/about-claude/models\n\ninterface ModelPricing {\n inputPerMillion: number;\n outputPerMillion: number;\n}\n\nconst PRICING: Record<string, ModelPricing> = {\n // Claude 4.x family\n \"claude-opus-4-20250514\": { inputPerMillion: 15, outputPerMillion: 75 },\n \"claude-sonnet-4-20250514\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-haiku-4-5-20251001\": { inputPerMillion: 0.8, outputPerMillion: 4 },\n\n // Claude 3.5 family (deprecated but still in use)\n \"claude-3-5-sonnet-20241022\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-5-sonnet-20240620\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-5-haiku-20241022\": { inputPerMillion: 0.8, outputPerMillion: 4 },\n\n // Claude 3 family (deprecated)\n \"claude-3-opus-20240229\": { inputPerMillion: 15, outputPerMillion: 75 },\n \"claude-3-sonnet-20240229\": { inputPerMillion: 3, outputPerMillion: 15 },\n \"claude-3-haiku-20240307\": { inputPerMillion: 0.25, outputPerMillion: 1.25 },\n};\n\n// Model alias resolution\nconst ALIASES: Record<string, string> = {\n \"claude-opus-4-0\": \"claude-opus-4-20250514\",\n \"claude-sonnet-4-0\": \"claude-sonnet-4-20250514\",\n \"claude-haiku-4-5-latest\": \"claude-haiku-4-5-20251001\",\n \"claude-3-5-sonnet-latest\": \"claude-3-5-sonnet-20241022\",\n \"claude-3-5-haiku-latest\": \"claude-3-5-haiku-20241022\",\n \"claude-3-opus-latest\": \"claude-3-opus-20240229\",\n \"claude-3-sonnet-latest\": \"claude-3-sonnet-20240229\",\n \"claude-3-haiku-latest\": \"claude-3-haiku-20240307\",\n};\n\nexport function calculateCost(\n model: string,\n inputTokens: number,\n outputTokens: number,\n): number {\n const resolvedModel = ALIASES[model] || model;\n const pricing = PRICING[resolvedModel];\n\n if (!pricing) {\n // Unknown model — return 0 rather than crashing the user's app\n return 0;\n }\n\n const inputCost = (inputTokens / 1_000_000) * pricing.inputPerMillion;\n const outputCost = (outputTokens / 1_000_000) * pricing.outputPerMillion;\n\n return Math.round((inputCost + outputCost) * 1_000_000) / 1_000_000; // 6 decimal places\n}\n","// Simple fast hash for prompt deduplication\n// Uses djb2 algorithm — not cryptographic, just for grouping identical prompts\n\nexport function hashString(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff;\n }\n return (hash >>> 0).toString(36);\n}\n","// Extract the caller's file + line number from the stack trace\n// Used for auto-tagging when no manual tag is provided\n\nexport function getCallSite(): string | null {\n const err = new Error();\n const stack = err.stack;\n if (!stack) return null;\n\n const lines = stack.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip the Error line itself\n if (trimmed.startsWith(\"Error\")) continue;\n\n // Skip costly SDK internals by matching on our known file names\n if (\n trimmed.includes(\"node_modules/costly/\") ||\n trimmed.includes(\"costlyWrappedMethod\") ||\n trimmed.includes(\"wrapMethod\") ||\n trimmed.includes(\"wrapClient\") ||\n trimmed.includes(\"wrapStream\") ||\n trimmed.includes(\"callsite.\") ||\n trimmed.includes(\"batcher.\") ||\n trimmed.includes(\"LogBatcher\")\n ) {\n continue;\n }\n\n // Extract file:line from the stack frame\n const match = trimmed.match(/\\((.+):(\\d+):\\d+\\)/) ||\n trimmed.match(/at (.+):(\\d+):\\d+/);\n\n if (match) {\n const file = match[1].replace(/^.*[/\\\\]/, \"\"); // basename only\n const line = match[2];\n return `${file}:${line}`;\n }\n }\n\n return null;\n}\n","import type { CostlyConfig, CostlyCallMetadata, RequestLog } from \"./types.js\";\nimport { LogBatcher } from \"./batcher.js\";\nimport { calculateCost } from \"./pricing.js\";\nimport { hashString } from \"./hash.js\";\nimport { getCallSite } from \"./callsite.js\";\n\nexport type { CostlyConfig, CostlyCallMetadata, RequestLog };\n\nconst DEFAULT_ENDPOINT = \"https://www.getcostly.dev/api/v1/ingest\";\nconst DEFAULT_FLUSH_INTERVAL = 5000; // 5 seconds\nconst DEFAULT_FLUSH_BATCH_SIZE = 10;\n\n// Methods to intercept on the Anthropic SDK\nconst INTERCEPTED_METHODS = new Set([\"create\", \"stream\"]);\n\ninterface CostlyClient {\n wrap<T extends object>(client: T): T;\n flush(): Promise<void>;\n shutdown(): Promise<void>;\n}\n\nexport function costly(config?: Partial<CostlyConfig>): CostlyClient {\n const apiKey = config?.apiKey ?? process.env.COSTLY_API_KEY;\n const projectId = config?.projectId ?? process.env.COSTLY_PROJECT_ID ?? \"_\";\n\n if (!apiKey) {\n throw new Error(\"[costly] apiKey is required — pass it in config or set COSTLY_API_KEY env var\");\n }\n\n const batcher = new LogBatcher({\n endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,\n apiKey,\n flushInterval: config?.flushInterval ?? DEFAULT_FLUSH_INTERVAL,\n flushBatchSize: config?.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,\n debug: config?.debug ?? false,\n });\n\n return {\n wrap<T extends object>(client: T): T {\n return wrapClient(client, projectId, batcher);\n },\n flush() {\n return batcher.flushAsync();\n },\n shutdown() {\n return batcher.shutdown();\n },\n };\n}\n\n// Cache proxies to avoid creating a new one on every property access\nconst proxyCache = new WeakMap<object, object>();\n\nfunction wrapClient<T extends object>(\n client: T,\n projectId: string,\n batcher: LogBatcher,\n): T {\n const cached = proxyCache.get(client);\n if (cached) return cached as T;\n\n const proxy = new Proxy(client, {\n get(target, prop, receiver) {\n const value = Reflect.get(target, prop, receiver);\n\n // Wrap intercepted methods (create, stream)\n if (INTERCEPTED_METHODS.has(prop as string) && typeof value === \"function\") {\n return wrapMethod(value.bind(target), projectId, batcher);\n }\n\n // Recursively wrap nested objects (e.g., anthropic.messages)\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return wrapClient(value as object, projectId, batcher);\n }\n\n return value;\n },\n });\n\n proxyCache.set(client, proxy);\n return proxy as T;\n}\n\nfunction wrapMethod(\n originalMethod: (...args: unknown[]) => unknown,\n projectId: string,\n batcher: LogBatcher,\n): (...args: unknown[]) => unknown {\n return function costlyWrappedMethod(...args: unknown[]): unknown {\n const callSite = getCallSite();\n const startTime = Date.now();\n\n // Extract and strip costly metadata from the request params\n const params = (args[0] ?? {}) as Record<string, unknown>;\n const costlyMeta = params.costly as CostlyCallMetadata | undefined;\n\n // Create a clean copy without the costly property\n if (costlyMeta) {\n const { costly: _, ...cleanParams } = params;\n args[0] = cleanParams;\n }\n\n // Extract data we need for logging before the call\n const model = (params.model as string) ?? \"unknown\";\n const maxTokens = (params.max_tokens as number) ?? null;\n const messages = params.messages as Array<{ role: string; content: unknown }> | undefined;\n const systemPrompt = params.system;\n\n // Build prompt hash from messages content\n const promptContent = messages\n ? JSON.stringify(messages.map((m) => ({ role: m.role, content: m.content })))\n : \"\";\n const promptHash = hashString(promptContent);\n const systemPromptHash = systemPrompt\n ? hashString(typeof systemPrompt === \"string\" ? systemPrompt : JSON.stringify(systemPrompt))\n : null;\n\n const tag = costlyMeta?.tag ?? null;\n const userId = costlyMeta?.userId ?? null;\n const autoCallSite = tag ? null : callSite;\n\n function buildLog(\n inputTokens: number,\n outputTokens: number,\n status: \"success\" | \"error\",\n errorType: string | null,\n ): RequestLog {\n return {\n projectId,\n timestamp: new Date().toISOString(),\n model,\n tag,\n userId,\n inputTokens,\n outputTokens,\n totalCost: calculateCost(model, inputTokens, outputTokens),\n maxTokens,\n status,\n errorType,\n promptHash,\n systemPromptHash,\n callSite: autoCallSite,\n durationMs: Date.now() - startTime,\n };\n }\n\n let result: unknown;\n try {\n result = originalMethod(...args);\n } catch (err) {\n // Synchronous error — log it but rethrow\n const error = err as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(0, 0, \"error\", error.error?.type ?? error.message ?? \"unknown_error\"));\n throw err;\n }\n\n // Handle thenable results (Promises, Streams with .finalMessage(), etc.)\n if (result && typeof (result as { then?: unknown }).then === \"function\") {\n (result as Promise<unknown>).then(\n (response) => {\n try {\n const res = response as Record<string, unknown>;\n const usage = res.usage as { input_tokens?: number; output_tokens?: number } | undefined;\n batcher.add(buildLog(usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, \"success\", null));\n } catch {\n // Never crash user code from logging\n }\n },\n (error) => {\n try {\n const err = error as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(0, 0, \"error\", err.error?.type ?? err.message ?? \"unknown_error\"));\n } catch {\n // Never crash user code from logging\n }\n },\n );\n }\n\n // If result is a streaming object (has Symbol.asyncIterator), wrap it to capture usage\n if (result && typeof (result as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === \"function\") {\n return wrapStream(result as AsyncIterable<unknown>, buildLog, batcher);\n }\n\n return result;\n };\n}\n\nfunction wrapStream(\n stream: AsyncIterable<unknown>,\n buildLog: (input: number, output: number, status: \"success\" | \"error\", errorType: string | null) => RequestLog,\n batcher: LogBatcher,\n): AsyncIterable<unknown> {\n const originalIterator = stream[Symbol.asyncIterator]();\n\n // Preserve all properties of the original stream object (e.g., .finalMessage(), .controller, etc.)\n const wrappedStream = Object.create(stream);\n\n wrappedStream[Symbol.asyncIterator] = () => {\n let inputTokens = 0;\n let outputTokens = 0;\n\n return {\n async next(): Promise<IteratorResult<unknown>> {\n try {\n const result = await originalIterator.next();\n\n if (result.done) {\n // Stream ended — log total usage\n batcher.add(buildLog(inputTokens, outputTokens, \"success\", null));\n return result;\n }\n\n // Accumulate usage from stream events\n const event = result.value as Record<string, unknown>;\n if (event.type === \"message_delta\") {\n const usage = event.usage as { output_tokens?: number } | undefined;\n if (usage?.output_tokens) {\n outputTokens = usage.output_tokens;\n }\n } else if (event.type === \"message_start\") {\n const message = event.message as { usage?: { input_tokens?: number } } | undefined;\n if (message?.usage?.input_tokens) {\n inputTokens = message.usage.input_tokens;\n }\n }\n\n return result;\n } catch (err) {\n const error = err as { error?: { type?: string }; message?: string };\n batcher.add(buildLog(inputTokens, outputTokens, \"error\", error.error?.type ?? error.message ?? \"unknown_error\"));\n throw err;\n }\n },\n async return(value?: unknown): Promise<IteratorResult<unknown>> {\n // Stream was cancelled early — still log what we have\n batcher.add(buildLog(inputTokens, outputTokens, \"success\", null));\n if (originalIterator.return) {\n return originalIterator.return(value);\n }\n return { done: true, value: undefined };\n },\n };\n };\n\n return wrappedStream;\n}\n"],"mappings":"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","names":["line"]}
|