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 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"))) return "bun";
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(["node_modules", ".next", "dist", "build", ".git", ".vercel", "coverage"]);
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("v0.1.0")}`);
197
+ log(` ${BOLD}costly${RESET} ${dim(`v${VERSION}`)}`);
181
198
  log("");
182
- const apiKey = await ask(` ${CYAN}?${RESET} Paste your API key ${dim("(from getcostly.dev/dashboard)")}: `);
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(` Install costly and add API key to .env anyway?`);
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(` Found ${found.length} file${found.length === 1 ? "" : "s"} using the Anthropic SDK:`);
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(` Wrap ${found.length === 1 ? "this file" : "these files"} with Costly?`);
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(` ${GREEN}${BOLD}You're all set.${RESET} Your dashboard will light up within 48 hours.`);
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("v0.1.0")}`);
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.flushBatchSize = opts.flushBatchSize;
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.flushBatchSize) {
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 DEFAULT_FLUSH_BATCH_SIZE = 10;
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
- flushBatchSize: config?.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,
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
@@ -2,8 +2,8 @@ interface CostlyConfig {
2
2
  projectId?: string;
3
3
  apiKey: string;
4
4
  endpoint?: string;
5
+ batchSize?: number;
5
6
  flushInterval?: number;
6
- flushBatchSize?: number;
7
7
  debug?: boolean;
8
8
  }
9
9
  interface CostlyCallMetadata {
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ interface CostlyConfig {
2
2
  projectId?: string;
3
3
  apiKey: string;
4
4
  endpoint?: string;
5
+ batchSize?: number;
5
6
  flushInterval?: number;
6
- flushBatchSize?: number;
7
7
  debug?: boolean;
8
8
  }
9
9
  interface CostlyCallMetadata {
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.flushBatchSize = opts.flushBatchSize;
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.flushBatchSize) {
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 DEFAULT_FLUSH_BATCH_SIZE = 10;
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
- flushBatchSize: config?.flushBatchSize ?? DEFAULT_FLUSH_BATCH_SIZE,
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.0",
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/dannytapia/costly"
50
+ "url": "https://github.com/itsdannyt/costly"
50
51
  },
51
52
  "keywords": [
52
53
  "anthropic",
@@ -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"]}