@burn0/burn0 0.1.0 → 0.2.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/index.js CHANGED
@@ -1156,8 +1156,8 @@ var require_command = __commonJS({
1156
1156
  "use strict";
1157
1157
  var EventEmitter = require("events").EventEmitter;
1158
1158
  var childProcess = require("child_process");
1159
- var path9 = require("path");
1160
- var fs7 = require("fs");
1159
+ var path10 = require("path");
1160
+ var fs8 = require("fs");
1161
1161
  var process5 = require("process");
1162
1162
  var { Argument: Argument2, humanReadableArgName } = require_argument();
1163
1163
  var { CommanderError: CommanderError2 } = require_error();
@@ -2138,7 +2138,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2138
2138
  * @param {string} subcommandName
2139
2139
  */
2140
2140
  _checkForMissingExecutable(executableFile, executableDir, subcommandName) {
2141
- if (fs7.existsSync(executableFile)) return;
2141
+ if (fs8.existsSync(executableFile)) return;
2142
2142
  const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
2143
2143
  const executableMissing = `'${executableFile}' does not exist
2144
2144
  - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
@@ -2156,11 +2156,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
2156
2156
  let launchWithNode = false;
2157
2157
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2158
2158
  function findFile(baseDir, baseName) {
2159
- const localBin = path9.resolve(baseDir, baseName);
2160
- if (fs7.existsSync(localBin)) return localBin;
2161
- if (sourceExt.includes(path9.extname(baseName))) return void 0;
2159
+ const localBin = path10.resolve(baseDir, baseName);
2160
+ if (fs8.existsSync(localBin)) return localBin;
2161
+ if (sourceExt.includes(path10.extname(baseName))) return void 0;
2162
2162
  const foundExt = sourceExt.find(
2163
- (ext) => fs7.existsSync(`${localBin}${ext}`)
2163
+ (ext) => fs8.existsSync(`${localBin}${ext}`)
2164
2164
  );
2165
2165
  if (foundExt) return `${localBin}${foundExt}`;
2166
2166
  return void 0;
@@ -2172,21 +2172,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
2172
2172
  if (this._scriptPath) {
2173
2173
  let resolvedScriptPath;
2174
2174
  try {
2175
- resolvedScriptPath = fs7.realpathSync(this._scriptPath);
2175
+ resolvedScriptPath = fs8.realpathSync(this._scriptPath);
2176
2176
  } catch {
2177
2177
  resolvedScriptPath = this._scriptPath;
2178
2178
  }
2179
- executableDir = path9.resolve(
2180
- path9.dirname(resolvedScriptPath),
2179
+ executableDir = path10.resolve(
2180
+ path10.dirname(resolvedScriptPath),
2181
2181
  executableDir
2182
2182
  );
2183
2183
  }
2184
2184
  if (executableDir) {
2185
2185
  let localFile = findFile(executableDir, executableFile);
2186
2186
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2187
- const legacyName = path9.basename(
2187
+ const legacyName = path10.basename(
2188
2188
  this._scriptPath,
2189
- path9.extname(this._scriptPath)
2189
+ path10.extname(this._scriptPath)
2190
2190
  );
2191
2191
  if (legacyName !== this._name) {
2192
2192
  localFile = findFile(
@@ -2197,7 +2197,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2197
2197
  }
2198
2198
  executableFile = localFile || executableFile;
2199
2199
  }
2200
- launchWithNode = sourceExt.includes(path9.extname(executableFile));
2200
+ launchWithNode = sourceExt.includes(path10.extname(executableFile));
2201
2201
  let proc;
2202
2202
  if (process5.platform !== "win32") {
2203
2203
  if (launchWithNode) {
@@ -3044,7 +3044,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3044
3044
  * @return {Command}
3045
3045
  */
3046
3046
  nameFromFilename(filename) {
3047
- this._name = path9.basename(filename, path9.extname(filename));
3047
+ this._name = path10.basename(filename, path10.extname(filename));
3048
3048
  return this;
3049
3049
  }
3050
3050
  /**
@@ -3058,9 +3058,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3058
3058
  * @param {string} [path]
3059
3059
  * @return {(string|null|Command)}
3060
3060
  */
3061
- executableDir(path10) {
3062
- if (path10 === void 0) return this._executableDir;
3063
- this._executableDir = path10;
3061
+ executableDir(path11) {
3062
+ if (path11 === void 0) return this._executableDir;
3063
+ this._executableDir = path11;
3064
3064
  return this;
3065
3065
  }
3066
3066
  /**
@@ -5096,15 +5096,15 @@ var require_route = __commonJS({
5096
5096
  };
5097
5097
  }
5098
5098
  function wrapConversion(toModel, graph) {
5099
- const path9 = [graph[toModel].parent, toModel];
5099
+ const path10 = [graph[toModel].parent, toModel];
5100
5100
  let fn = conversions[graph[toModel].parent][toModel];
5101
5101
  let cur = graph[toModel].parent;
5102
5102
  while (graph[cur].parent) {
5103
- path9.unshift(graph[cur].parent);
5103
+ path10.unshift(graph[cur].parent);
5104
5104
  fn = link(conversions[graph[cur].parent][cur], fn);
5105
5105
  cur = graph[cur].parent;
5106
5106
  }
5107
- fn.conversion = path9;
5107
+ fn.conversion = path10;
5108
5108
  return fn;
5109
5109
  }
5110
5110
  module2.exports = function(fromModel) {
@@ -11997,10 +11997,10 @@ var require_lib2 = __commonJS({
11997
11997
  exports2.analyse = analyse;
11998
11998
  var detectFile = (filepath, opts = {}) => new Promise((resolve, reject) => {
11999
11999
  let fd;
12000
- const fs7 = (0, node_1.default)();
12000
+ const fs8 = (0, node_1.default)();
12001
12001
  const handler = (err, buffer) => {
12002
12002
  if (fd) {
12003
- fs7.closeSync(fd);
12003
+ fs8.closeSync(fd);
12004
12004
  }
12005
12005
  if (err) {
12006
12006
  reject(err);
@@ -12012,9 +12012,9 @@ var require_lib2 = __commonJS({
12012
12012
  };
12013
12013
  const sampleSize = (opts === null || opts === void 0 ? void 0 : opts.sampleSize) || 0;
12014
12014
  if (sampleSize > 0) {
12015
- fd = fs7.openSync(filepath, "r");
12015
+ fd = fs8.openSync(filepath, "r");
12016
12016
  let sample = Buffer.allocUnsafe(sampleSize);
12017
- fs7.read(fd, sample, 0, sampleSize, opts.offset, (err, bytesRead) => {
12017
+ fs8.read(fd, sample, 0, sampleSize, opts.offset, (err, bytesRead) => {
12018
12018
  if (err) {
12019
12019
  handler(err, null);
12020
12020
  } else {
@@ -12026,22 +12026,22 @@ var require_lib2 = __commonJS({
12026
12026
  });
12027
12027
  return;
12028
12028
  }
12029
- fs7.readFile(filepath, handler);
12029
+ fs8.readFile(filepath, handler);
12030
12030
  });
12031
12031
  exports2.detectFile = detectFile;
12032
12032
  var detectFileSync = (filepath, opts = {}) => {
12033
- const fs7 = (0, node_1.default)();
12033
+ const fs8 = (0, node_1.default)();
12034
12034
  if (opts && opts.sampleSize) {
12035
- const fd = fs7.openSync(filepath, "r");
12035
+ const fd = fs8.openSync(filepath, "r");
12036
12036
  let sample = Buffer.allocUnsafe(opts.sampleSize);
12037
- const bytesRead = fs7.readSync(fd, sample, 0, opts.sampleSize, opts.offset);
12037
+ const bytesRead = fs8.readSync(fd, sample, 0, opts.sampleSize, opts.offset);
12038
12038
  if (bytesRead < opts.sampleSize) {
12039
12039
  sample = sample.subarray(0, bytesRead);
12040
12040
  }
12041
- fs7.closeSync(fd);
12041
+ fs8.closeSync(fd);
12042
12042
  return (0, exports2.detect)(sample);
12043
12043
  }
12044
- return (0, exports2.detect)(fs7.readFileSync(filepath));
12044
+ return (0, exports2.detect)(fs8.readFileSync(filepath));
12045
12045
  };
12046
12046
  exports2.detectFileSync = detectFileSync;
12047
12047
  exports2.default = {
@@ -17948,9 +17948,97 @@ var init_catalog = __esm({
17948
17948
  }
17949
17949
  });
17950
17950
 
17951
+ // src/cli/api-key.ts
17952
+ function openBrowser(url) {
17953
+ try {
17954
+ const platform = process.platform;
17955
+ if (platform === "darwin") (0, import_node_child_process.execFileSync)("open", [url]);
17956
+ else if (platform === "win32") (0, import_node_child_process.execFileSync)("cmd", ["/c", "start", url]);
17957
+ else (0, import_node_child_process.execFileSync)("xdg-open", [url]);
17958
+ return true;
17959
+ } catch {
17960
+ return false;
17961
+ }
17962
+ }
17963
+ async function promptApiKey(cwd) {
17964
+ const choice = await esm_default11({
17965
+ message: "API key?",
17966
+ choices: [
17967
+ { name: "Paste key", value: "paste" },
17968
+ { name: "Get one free \u2192 burn0.dev/api", value: "get" },
17969
+ { name: "Skip \u2014 local mode", value: "skip" }
17970
+ ]
17971
+ });
17972
+ if (choice === "skip") return void 0;
17973
+ if (choice === "get") {
17974
+ const opened = openBrowser("https://burn0.dev/api");
17975
+ if (!opened) {
17976
+ console.log(source_default.dim("\n Visit burn0.dev/api to get your free API key\n"));
17977
+ } else {
17978
+ console.log(source_default.dim("\n Opening burn0.dev/api in your browser...\n"));
17979
+ }
17980
+ }
17981
+ while (true) {
17982
+ const apiKey = await esm_default5({ message: "Paste your API key:" });
17983
+ if (!apiKey || !apiKey.startsWith("b0_sk_")) {
17984
+ console.log(source_default.red("\n Invalid key. Keys start with b0_sk_"));
17985
+ const retry = await esm_default11({
17986
+ message: "What would you like to do?",
17987
+ choices: [
17988
+ { name: "Try again", value: "retry" },
17989
+ { name: "Skip \u2014 local mode", value: "skip" }
17990
+ ]
17991
+ });
17992
+ if (retry === "skip") return void 0;
17993
+ continue;
17994
+ }
17995
+ writeApiKeyToEnv(cwd, apiKey);
17996
+ console.log(source_default.green(" \u2713 Added BURN0_API_KEY to .env"));
17997
+ return apiKey;
17998
+ }
17999
+ }
18000
+ function writeApiKeyToEnv(cwd, apiKey) {
18001
+ const envPath = import_node_path5.default.join(cwd, ".env");
18002
+ const examplePath = import_node_path5.default.join(cwd, ".env.example");
18003
+ let envContent = "";
18004
+ try {
18005
+ envContent = import_node_fs4.default.readFileSync(envPath, "utf-8");
18006
+ } catch {
18007
+ }
18008
+ if (envContent.includes("BURN0_API_KEY=")) {
18009
+ envContent = envContent.replace(/BURN0_API_KEY=.*/, `BURN0_API_KEY=${apiKey}`);
18010
+ } else {
18011
+ envContent += `${envContent && !envContent.endsWith("\n") ? "\n" : ""}BURN0_API_KEY=${apiKey}
18012
+ `;
18013
+ }
18014
+ import_node_fs4.default.writeFileSync(envPath, envContent);
18015
+ let exampleContent = "";
18016
+ try {
18017
+ exampleContent = import_node_fs4.default.readFileSync(examplePath, "utf-8");
18018
+ } catch {
18019
+ }
18020
+ if (!exampleContent.includes("BURN0_API_KEY=")) {
18021
+ exampleContent += `${exampleContent && !exampleContent.endsWith("\n") ? "\n" : ""}BURN0_API_KEY=
18022
+ `;
18023
+ import_node_fs4.default.writeFileSync(examplePath, exampleContent);
18024
+ }
18025
+ }
18026
+ var import_node_child_process, import_node_fs4, import_node_path5;
18027
+ var init_api_key = __esm({
18028
+ "src/cli/api-key.ts"() {
18029
+ "use strict";
18030
+ init_esm15();
18031
+ init_source();
18032
+ import_node_child_process = require("child_process");
18033
+ import_node_fs4 = __toESM(require("fs"));
18034
+ import_node_path5 = __toESM(require("path"));
18035
+ }
18036
+ });
18037
+
17951
18038
  // src/cli/init.ts
17952
18039
  var init_exports = {};
17953
18040
  __export(init_exports, {
18041
+ ensureGitignore: () => ensureGitignore,
17954
18042
  runInit: () => runInit
17955
18043
  });
17956
18044
  async function runInit() {
@@ -17958,7 +18046,7 @@ async function runInit() {
17958
18046
  await _runInit();
17959
18047
  } catch (err) {
17960
18048
  if (err.name === "ExitPromptError" || err.message?.includes("SIGINT")) {
17961
- console.log("\n\n Cancelled. Run `burn0 init` again when ready.\n");
18049
+ console.log("\n\n Cancelled. Run `npx burn0 init` when ready.\n");
17962
18050
  process.exit(0);
17963
18051
  }
17964
18052
  throw err;
@@ -17966,160 +18054,115 @@ async function runInit() {
17966
18054
  }
17967
18055
  async function _runInit() {
17968
18056
  const cwd = process.cwd();
17969
- const o = source_default.hex("#FA5D19");
17970
- const banner = `
17971
- ${"bbbbbbbb"}
17972
- ${"b::::::b"} ${o("000000000")}
17973
- ${"b::::::b"} ${o("00:::::::::00")}
17974
- ${"b::::::b"} ${o("00:::::::::::::00")}
17975
- ${" b:::::b"} ${o("0:::::::000:::::::0")}
17976
- ${" b:::::bbbbbbbbb"} ${"uuuuuu uuuuuu"} ${"rrrrr rrrrrrrrr"} ${"nnnn nnnnnnnn"} ${o("0::::::0 0::::::0")}
17977
- ${" b::::::::::::::bb"} ${"u::::u u::::u"} ${"r::::rrr:::::::::r"} ${"n:::nn::::::::nn"} ${o("0:::::0 0:::::0")}
17978
- ${" b::::::::::::::::b"} ${"u::::u u::::u"} ${"r:::::::::::::::::r"} ${"n::::::::::::::nn"} ${o("0:::::0 0:::::0")}
17979
- ${" b:::::bbbbb:::::::b"}${"u::::u u::::u"} ${"rr::::::rrrrr::::::r"}${"nn:::::::::::::::n"}${o("0:::::0 000 0:::::0")}
17980
- ${" b:::::b b::::::b"}${"u::::u u::::u"} ${"r:::::r r:::::r"} ${"n:::::nnnn:::::n"}${o("0:::::0 000 0:::::0")}
17981
- ${" b:::::b b:::::b"}${"u::::u u::::u"} ${"r:::::r rrrrrrr"} ${"n::::n n::::n"}${o("0:::::0 0:::::0")}
17982
- ${" b:::::b b:::::b"}${"u::::u u::::u"} ${"r:::::r"} ${"n::::n n::::n"}${o("0:::::0 0:::::0")}
17983
- ${" b:::::b b:::::b"}${"u:::::uuuu:::::u"} ${"r:::::r"} ${"n::::n n::::n"}${o("0::::::0 0::::::0")}
17984
- ${" b:::::bbbbbb::::::b"}${"u:::::::::::::::uu"}${"r:::::r"} ${"n::::n n::::n"}${o("0:::::::000:::::::0")}
17985
- ${" b::::::::::::::::b"} ${"u:::::::::::::::u"}${"r:::::r"} ${"n::::n n::::n"} ${o("00:::::::::::::00")}
17986
- ${" b:::::::::::::::b"} ${"uu::::::::uu:::u"}${"r:::::r"} ${"n::::n n::::n"} ${o("00:::::::::00")}
17987
- ${" bbbbbbbbbbbbbbbb"} ${"uuuuuuuu uuuu"}${"rrrrrrr"} ${"nnnnnn nnnnnn"} ${o("000000000")}
17988
- `;
17989
- console.log(banner);
17990
- console.log(source_default.dim(" Track every API call. Know your costs.\n"));
17991
- console.log(source_default.dim(" Scanning your project...\n"));
17992
- const services = detectServices(cwd);
17993
- if (services.length === 0) {
17994
- console.log(source_default.yellow(" No known API services found in package.json."));
17995
- console.log(source_default.dim(" burn0 will still track any outgoing HTTP calls.\n"));
17996
- } else {
17997
- console.log(source_default.bold(` Detected ${services.length} services:
17998
- `));
17999
- console.log(source_default.dim(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
18000
- for (const svc of services) {
18001
- const label = svc.autopriced ? source_default.green(" \u2713") : source_default.yellow(" \u25C6");
18002
- const category = svc.category === "llm" ? source_default.blue("LLM") : svc.autopriced ? source_default.magenta("API") : source_default.yellow("API");
18003
- const pricing = svc.autopriced ? source_default.dim("auto-priced") : source_default.yellow("plan needed");
18004
- console.log(`${label} ${svc.package.padEnd(25)} ${category} ${pricing}`);
18005
- }
18006
- console.log(source_default.dim(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
18007
- console.log();
18008
- }
18009
- console.log(source_default.dim(" Scanning your codebase for API usage...\n"));
18057
+ console.log(source_default.dim("\n burn0 \u2014 track every API cost\n"));
18058
+ const apiKey = await promptApiKey(cwd);
18059
+ console.log(source_default.dim("\n Scanning your project...\n"));
18060
+ const pkgServices = detectServices(cwd);
18010
18061
  const scannedServices = scanCodebase(cwd);
18011
- const detectedNames = new Set(services.map((s) => s.name));
18062
+ const detectedNames = new Set(pkgServices.map((s) => s.name));
18012
18063
  const newFromScan = scannedServices.filter((s) => !detectedNames.has(s.name));
18013
- if (newFromScan.length > 0) {
18014
- console.log(source_default.bold(` Found ${newFromScan.length} more services in your code:
18015
- `));
18016
- console.log(source_default.dim(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
18017
- for (const svc of newFromScan) {
18018
- const catalogEntry = SERVICE_CATALOG.find((c) => c.name === svc.name);
18019
- const displayName = catalogEntry?.displayName ?? svc.name;
18020
- const files = svc.foundIn.slice(0, 3).join(", ");
18021
- const more = svc.foundIn.length > 3 ? ` +${svc.foundIn.length - 3} more` : "";
18022
- console.log(` ${source_default.yellow(" \u25C6")} ${displayName.padEnd(20)} ${source_default.dim(`found in: ${files}${more}`)}`);
18023
- }
18024
- console.log(source_default.dim(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
18025
- console.log();
18026
- for (const svc of newFromScan) {
18027
- detectedNames.add(svc.name);
18028
- }
18029
- } else {
18030
- console.log(source_default.dim(" No additional services found in codebase.\n"));
18064
+ const allDetected = [];
18065
+ for (const svc of pkgServices) {
18066
+ const entry = SERVICE_CATALOG.find((c) => c.name === svc.name);
18067
+ allDetected.push({
18068
+ name: svc.name,
18069
+ displayName: entry?.displayName ?? svc.name,
18070
+ autopriced: svc.autopriced
18071
+ });
18031
18072
  }
18032
- const keyChoice = await esm_default11({
18033
- message: "Do you have a burn0 API key? (get one free at burn0.dev)",
18034
- choices: [
18035
- { name: "Yes \u2014 paste it", value: "yes" },
18036
- { name: "Skip \u2014 use local mode for now", value: "skip" }
18037
- ]
18038
- });
18039
- let apiKey;
18040
- if (keyChoice === "yes") {
18041
- apiKey = await esm_default5({ message: "Paste your API key:" });
18042
- writeApiKeyToEnv(cwd, apiKey);
18043
- console.log(source_default.green(" \u2713 Added BURN0_API_KEY to .env"));
18073
+ for (const svc of newFromScan) {
18074
+ const entry = SERVICE_CATALOG.find((c) => c.name === svc.name);
18075
+ allDetected.push({
18076
+ name: svc.name,
18077
+ displayName: entry?.displayName ?? svc.name,
18078
+ autopriced: entry?.pricingType !== "fixed"
18079
+ });
18044
18080
  }
18045
18081
  const serviceConfigs = [];
18046
- const detectedFixedTier = services.filter((s) => !s.autopriced);
18047
- const scannedFixedTier = newFromScan.filter((s) => {
18048
- const entry = SERVICE_CATALOG.find((c) => c.name === s.name);
18049
- return entry?.pricingType === "fixed";
18050
- });
18051
- const allFixedTier = [
18052
- ...detectedFixedTier.map((s) => s.name),
18053
- ...scannedFixedTier.map((s) => s.name)
18054
- ];
18055
- for (const name of allFixedTier) {
18056
- const catalogEntry = SERVICE_CATALOG.find((c) => c.name === name);
18057
- if (catalogEntry?.plans) {
18058
- const plan = await esm_default11({
18059
- message: `${catalogEntry.displayName} \u2014 which plan are you on?`,
18060
- choices: [
18061
- ...catalogEntry.plans.map((p) => ({ name: p.name, value: p.value })),
18062
- { name: "Skip \u2014 I'll set this up later", value: "skip" }
18063
- ]
18064
- });
18065
- if (plan !== "skip") {
18066
- const selected = catalogEntry.plans.find((p) => p.value === plan);
18067
- serviceConfigs.push({ name, plan, monthlyCost: selected?.monthly });
18082
+ if (allDetected.length > 0) {
18083
+ console.log(source_default.bold(` Auto-detected ${allDetected.length} services:
18084
+ `));
18085
+ for (const svc of allDetected) {
18086
+ const tag = svc.autopriced ? source_default.dim("auto-priced") : source_default.yellow("needs plan");
18087
+ console.log(` ${source_default.green(" \u2713")} ${svc.displayName.padEnd(20)} ${tag}`);
18088
+ }
18089
+ console.log();
18090
+ const fixedTier = allDetected.filter((s) => !s.autopriced);
18091
+ if (fixedTier.length > 0) {
18092
+ for (const svc of fixedTier) {
18093
+ const entry = SERVICE_CATALOG.find((c) => c.name === svc.name);
18094
+ if (entry?.plans) {
18095
+ const plan = await esm_default11({
18096
+ message: `${entry.displayName} \u2014 which plan?`,
18097
+ choices: [
18098
+ ...entry.plans.map((p) => ({ name: p.name, value: p.value })),
18099
+ { name: "Skip", value: "skip" }
18100
+ ]
18101
+ });
18102
+ if (plan !== "skip") {
18103
+ const selected = entry.plans.find((p) => p.value === plan);
18104
+ serviceConfigs.push({ name: svc.name, plan, monthlyCost: selected?.monthly });
18105
+ } else {
18106
+ serviceConfigs.push({ name: svc.name });
18107
+ }
18108
+ }
18068
18109
  }
18069
18110
  }
18111
+ for (const svc of allDetected.filter((s) => s.autopriced)) {
18112
+ serviceConfigs.push({ name: svc.name });
18113
+ }
18114
+ } else {
18115
+ console.log(source_default.dim(" No services detected.\n"));
18070
18116
  }
18071
- const additionalServices = SERVICE_CATALOG.filter((s) => !detectedNames.has(s.name));
18072
18117
  const addMore = await esm_default4({
18073
- message: "Do you use any other paid APIs or services? (not detected from package.json)",
18118
+ message: "Add other services you use?",
18074
18119
  default: false
18075
18120
  });
18076
18121
  if (addMore) {
18077
- const llmChoices = additionalServices.filter((s) => s.category === "llm").map((s) => ({ name: `${s.displayName}`, value: s.name }));
18078
- const apiChoices = additionalServices.filter((s) => s.category === "api").map((s) => ({ name: `${s.displayName}`, value: s.name }));
18079
- const infraChoices = additionalServices.filter((s) => s.category === "infra").map((s) => ({ name: `${s.displayName}`, value: s.name }));
18080
- const selected = await esm_default2({
18081
- message: "Select all services you use (space to select, enter to confirm)",
18122
+ const alreadyAdded = new Set(serviceConfigs.map((s) => s.name));
18123
+ const additionalServices = SERVICE_CATALOG.filter((s) => !alreadyAdded.has(s.name));
18124
+ const llmChoices = additionalServices.filter((s) => s.category === "llm").map((s) => ({ name: s.displayName, value: s.name }));
18125
+ const apiChoices = additionalServices.filter((s) => s.category === "api").map((s) => ({ name: s.displayName, value: s.name }));
18126
+ const infraChoices = additionalServices.filter((s) => s.category === "infra").map((s) => ({ name: s.displayName, value: s.name }));
18127
+ const additional = await esm_default2({
18128
+ message: "Select services:",
18082
18129
  choices: [
18083
- ...llmChoices.length ? [{ name: source_default.bold.blue("\u2500\u2500 LLM Providers \u2500\u2500"), value: "__sep_llm", disabled: true }] : [],
18130
+ ...llmChoices.length ? [{ name: source_default.bold.blue("\u2500\u2500 LLM Providers \u2500\u2500"), value: "__sep", disabled: true }] : [],
18084
18131
  ...llmChoices,
18085
- ...apiChoices.length ? [{ name: source_default.bold.magenta("\u2500\u2500 API Services \u2500\u2500"), value: "__sep_api", disabled: true }] : [],
18132
+ ...apiChoices.length ? [{ name: source_default.bold.magenta("\u2500\u2500 API Services \u2500\u2500"), value: "__sep2", disabled: true }] : [],
18086
18133
  ...apiChoices,
18087
- ...infraChoices.length ? [{ name: source_default.bold.yellow("\u2500\u2500 Infrastructure \u2500\u2500"), value: "__sep_infra", disabled: true }] : [],
18134
+ ...infraChoices.length ? [{ name: source_default.bold.yellow("\u2500\u2500 Infrastructure \u2500\u2500"), value: "__sep3", disabled: true }] : [],
18088
18135
  ...infraChoices
18089
18136
  ]
18090
18137
  });
18091
- for (const name of selected) {
18092
- if (name.startsWith("__sep_")) continue;
18093
- const catalogEntry = SERVICE_CATALOG.find((c) => c.name === name);
18094
- if (!catalogEntry) continue;
18095
- if (catalogEntry.pricingType === "fixed" && catalogEntry.plans) {
18138
+ for (const name of additional) {
18139
+ if (name.startsWith("__sep")) continue;
18140
+ const entry = SERVICE_CATALOG.find((c) => c.name === name);
18141
+ if (entry?.pricingType === "fixed" && entry.plans) {
18096
18142
  const plan = await esm_default11({
18097
- message: `${catalogEntry.displayName} \u2014 which plan are you on?`,
18143
+ message: `${entry.displayName} \u2014 which plan?`,
18098
18144
  choices: [
18099
- ...catalogEntry.plans.map((p) => ({ name: p.name, value: p.value })),
18100
- { name: "Skip \u2014 I'll set this up later", value: "skip" }
18145
+ ...entry.plans.map((p) => ({ name: p.name, value: p.value })),
18146
+ { name: "Skip", value: "skip" }
18101
18147
  ]
18102
18148
  });
18103
18149
  if (plan !== "skip") {
18104
- const selectedPlan = catalogEntry.plans.find((p) => p.value === plan);
18105
- serviceConfigs.push({ name, plan, monthlyCost: selectedPlan?.monthly });
18150
+ const selected = entry.plans.find((p) => p.value === plan);
18151
+ serviceConfigs.push({ name, plan, monthlyCost: selected?.monthly });
18152
+ } else {
18153
+ serviceConfigs.push({ name });
18106
18154
  }
18107
18155
  } else {
18108
18156
  serviceConfigs.push({ name });
18109
18157
  }
18110
18158
  }
18111
18159
  }
18112
- const pkgJsonPath = import_node_path5.default.join(cwd, "package.json");
18113
- let defaultName = "my-project";
18160
+ let projectName = "my-project";
18114
18161
  try {
18115
- const pkg = JSON.parse(import_node_fs4.default.readFileSync(pkgJsonPath, "utf-8"));
18116
- if (pkg.name) defaultName = pkg.name;
18162
+ const pkg = JSON.parse(import_node_fs5.default.readFileSync(import_node_path6.default.join(cwd, "package.json"), "utf-8"));
18163
+ if (pkg.name) projectName = pkg.name;
18117
18164
  } catch {
18118
18165
  }
18119
- const projectName = await esm_default5({
18120
- message: "Project name? (used in dashboard)",
18121
- default: defaultName
18122
- });
18123
18166
  writeConfig(cwd, {
18124
18167
  projectName,
18125
18168
  services: serviceConfigs.map((s) => ({
@@ -18129,87 +18172,78 @@ async function _runInit() {
18129
18172
  monthlyCost: s.monthlyCost
18130
18173
  }))
18131
18174
  });
18175
+ if (apiKey) {
18176
+ try {
18177
+ const apiUrl = process.env.BURN0_API_URL ?? "https://api.burn0.dev";
18178
+ const res = await fetch(`${apiUrl}/v1/projects/config`, {
18179
+ method: "POST",
18180
+ headers: {
18181
+ "Content-Type": "application/json",
18182
+ "Authorization": `Bearer ${apiKey}`
18183
+ },
18184
+ body: JSON.stringify({
18185
+ services: serviceConfigs.map((s) => ({
18186
+ name: s.name,
18187
+ pricingModel: s.plan ? "fixed-tier" : "auto",
18188
+ plan: s.plan,
18189
+ monthlyCost: s.monthlyCost
18190
+ }))
18191
+ })
18192
+ });
18193
+ if (res.ok) {
18194
+ console.log(source_default.green(" \u2713 Config synced to burn0.dev"));
18195
+ }
18196
+ } catch {
18197
+ }
18198
+ }
18132
18199
  ensureGitignore(cwd, ".burn0/");
18133
- const gitignorePath = import_node_path5.default.join(cwd, ".gitignore");
18200
+ const gitignorePath = import_node_path6.default.join(cwd, ".gitignore");
18134
18201
  let gitignoreContent = "";
18135
18202
  try {
18136
- gitignoreContent = import_node_fs4.default.readFileSync(gitignorePath, "utf-8");
18203
+ gitignoreContent = import_node_fs5.default.readFileSync(gitignorePath, "utf-8");
18137
18204
  } catch {
18138
18205
  }
18139
- if (!gitignoreContent.includes(".env")) {
18140
- const addEnv = await esm_default4({ message: ".env is not in .gitignore. Add it?" });
18141
- if (addEnv) ensureGitignore(cwd, ".env");
18206
+ const gitignoreLines = gitignoreContent.split("\n").map((l) => l.trim());
18207
+ if (!gitignoreLines.includes(".env")) {
18208
+ ensureGitignore(cwd, ".env");
18209
+ console.log(source_default.green(" \u2713 Added .env to .gitignore (protects your API keys)"));
18142
18210
  }
18143
18211
  console.log("");
18144
- console.log(source_default.green(" \u2713 Config written to .burn0/config.json"));
18145
- console.log(source_default.green(" \u2713 Added .burn0/ to .gitignore"));
18212
+ console.log(source_default.green(" \u2713 Setup complete"));
18146
18213
  console.log("");
18147
- console.log(source_default.bold(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
18148
- console.log(source_default.bold(" \u2502") + source_default.dim(" Next steps: ") + source_default.bold("\u2502"));
18149
- console.log(source_default.bold(" \u2502 \u2502"));
18150
- console.log(source_default.bold(" \u2502") + source_default.cyan(" 1. Add to your app entry point: ") + source_default.bold("\u2502"));
18151
- console.log(source_default.bold(" \u2502") + source_default.white(" import 'burn0' ") + source_default.bold("\u2502"));
18152
- console.log(source_default.bold(" \u2502 \u2502"));
18153
- console.log(source_default.bold(" \u2502") + source_default.cyan(" 2. Optional \u2014 track specific features: ") + source_default.bold("\u2502"));
18154
- console.log(source_default.bold(" \u2502") + source_default.white(" import { track } from 'burn0' ") + source_default.bold("\u2502"));
18155
- console.log(source_default.bold(" \u2502") + source_default.white(" track('feat', { userId }, async () => {})") + source_default.bold("\u2502"));
18156
- console.log(source_default.bold(" \u2502 \u2502"));
18157
- console.log(source_default.bold(" \u2502") + source_default.cyan(" 3. Check your costs: ") + source_default.bold("\u2502"));
18158
- console.log(source_default.bold(" \u2502") + source_default.white(" burn0 report ") + source_default.bold("\u2502"));
18159
- console.log(source_default.bold(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
18214
+ console.log(source_default.dim(" Add this to your entry file:"));
18215
+ console.log(source_default.white(" import '@burn0/burn0'"));
18216
+ console.log("");
18217
+ console.log(source_default.dim(" Then run your app to see costs."));
18160
18218
  console.log("");
18161
- }
18162
- function writeApiKeyToEnv(cwd, apiKey) {
18163
- const envPath = import_node_path5.default.join(cwd, ".env");
18164
- const examplePath = import_node_path5.default.join(cwd, ".env.example");
18165
- let envContent = "";
18166
- try {
18167
- envContent = import_node_fs4.default.readFileSync(envPath, "utf-8");
18168
- } catch {
18169
- }
18170
- if (envContent.includes("BURN0_API_KEY=")) {
18171
- envContent = envContent.replace(/BURN0_API_KEY=.*/, `BURN0_API_KEY=${apiKey}`);
18172
- } else {
18173
- envContent += `${envContent && !envContent.endsWith("\n") ? "\n" : ""}BURN0_API_KEY=${apiKey}
18174
- `;
18175
- }
18176
- import_node_fs4.default.writeFileSync(envPath, envContent);
18177
- let exampleContent = "";
18178
- try {
18179
- exampleContent = import_node_fs4.default.readFileSync(examplePath, "utf-8");
18180
- } catch {
18181
- }
18182
- if (!exampleContent.includes("BURN0_API_KEY=")) {
18183
- exampleContent += `${exampleContent && !exampleContent.endsWith("\n") ? "\n" : ""}BURN0_API_KEY=
18184
- `;
18185
- import_node_fs4.default.writeFileSync(examplePath, exampleContent);
18186
- }
18187
18219
  }
18188
18220
  function ensureGitignore(cwd, entry) {
18189
- const gitignorePath = import_node_path5.default.join(cwd, ".gitignore");
18221
+ const gitignorePath = import_node_path6.default.join(cwd, ".gitignore");
18190
18222
  let content = "";
18191
18223
  try {
18192
- content = import_node_fs4.default.readFileSync(gitignorePath, "utf-8");
18224
+ content = import_node_fs5.default.readFileSync(gitignorePath, "utf-8");
18193
18225
  } catch {
18194
18226
  }
18195
- if (!content.includes(entry)) {
18227
+ const lines = content.split("\n").map((l) => l.trim());
18228
+ if (!lines.includes(entry)) {
18196
18229
  content += `${content && !content.endsWith("\n") ? "\n" : ""}${entry}
18197
18230
  `;
18198
- import_node_fs4.default.writeFileSync(gitignorePath, content);
18231
+ import_node_fs5.default.writeFileSync(gitignorePath, content);
18199
18232
  }
18200
18233
  }
18201
- var import_node_fs4, import_node_path5;
18234
+ var import_node_fs5, import_node_path6;
18202
18235
  var init_init = __esm({
18203
18236
  "src/cli/init.ts"() {
18204
18237
  "use strict";
18205
18238
  init_esm15();
18206
18239
  init_source();
18207
- import_node_fs4 = __toESM(require("fs"));
18208
- import_node_path5 = __toESM(require("path"));
18240
+ import_node_fs5 = __toESM(require("fs"));
18241
+ import_node_path6 = __toESM(require("path"));
18209
18242
  init_detect();
18210
18243
  init_scan();
18211
18244
  init_store();
18212
18245
  init_catalog();
18246
+ init_api_key();
18213
18247
  }
18214
18248
  });
18215
18249
 
@@ -18269,43 +18303,17 @@ __export(connect_exports, {
18269
18303
  });
18270
18304
  async function runConnect() {
18271
18305
  const cwd = process.cwd();
18272
- const apiKey = await esm_default5({ message: "Paste your burn0 API key:" });
18273
- if (!apiKey || !apiKey.startsWith("b0_sk_")) {
18274
- console.log(source_default.red("\n Invalid key. Keys start with b0_sk_\n"));
18275
- return;
18276
- }
18277
- const envPath = import_node_path6.default.join(cwd, ".env");
18278
- let content = "";
18279
- try {
18280
- content = import_node_fs5.default.readFileSync(envPath, "utf-8");
18281
- } catch {
18282
- }
18283
- if (content.includes("BURN0_API_KEY=")) {
18284
- content = content.replace(/BURN0_API_KEY=.*/, `BURN0_API_KEY=${apiKey}`);
18285
- } else {
18286
- content += `${content && !content.endsWith("\n") ? "\n" : ""}BURN0_API_KEY=${apiKey}
18287
- `;
18288
- }
18289
- import_node_fs5.default.writeFileSync(envPath, content);
18290
- console.log(source_default.green("\n \u2713 Added BURN0_API_KEY to .env"));
18291
- try {
18292
- const pkg = JSON.parse(import_node_fs5.default.readFileSync(import_node_path6.default.join(cwd, "package.json"), "utf-8"));
18293
- if (pkg.name) {
18294
- console.log(source_default.green(` \u2713 Connected to project "${pkg.name}" on burn0.dev
18295
- `));
18296
- }
18297
- } catch {
18298
- console.log(source_default.green(" \u2713 Connected to burn0.dev\n"));
18306
+ const key = await promptApiKey(cwd);
18307
+ if (key) {
18308
+ ensureGitignore(cwd, ".env");
18309
+ ensureGitignore(cwd, ".burn0/");
18299
18310
  }
18300
18311
  }
18301
- var import_node_fs5, import_node_path6;
18302
18312
  var init_connect = __esm({
18303
18313
  "src/cli/connect.ts"() {
18304
18314
  "use strict";
18305
- init_esm15();
18306
- init_source();
18307
- import_node_fs5 = __toESM(require("fs"));
18308
- import_node_path6 = __toESM(require("path"));
18315
+ init_api_key();
18316
+ init_init();
18309
18317
  }
18310
18318
  });
18311
18319
 
@@ -18370,46 +18378,297 @@ var init_local = __esm({
18370
18378
  }
18371
18379
  });
18372
18380
 
18381
+ // src/transport/local-pricing.ts
18382
+ async function fetchPricing(apiUrl, fetchFn) {
18383
+ try {
18384
+ const cachePath = import_node_path8.default.join(process.cwd(), CACHE_FILE);
18385
+ if (import_node_fs7.default.existsSync(cachePath)) {
18386
+ const raw = import_node_fs7.default.readFileSync(cachePath, "utf-8");
18387
+ const cached = JSON.parse(raw);
18388
+ if (Date.now() - cached.cached_at < CACHE_TTL_MS) {
18389
+ pricingData = cached;
18390
+ return;
18391
+ }
18392
+ }
18393
+ } catch {
18394
+ }
18395
+ try {
18396
+ const response = await fetchFn(`${apiUrl}/v1/pricing`, {
18397
+ headers: { "Accept": "application/json" }
18398
+ });
18399
+ if (response.ok) {
18400
+ const data = await response.json();
18401
+ pricingData = data;
18402
+ try {
18403
+ const dir = import_node_path8.default.join(process.cwd(), ".burn0");
18404
+ if (!import_node_fs7.default.existsSync(dir)) import_node_fs7.default.mkdirSync(dir, { recursive: true });
18405
+ import_node_fs7.default.writeFileSync(
18406
+ import_node_path8.default.join(process.cwd(), CACHE_FILE),
18407
+ JSON.stringify({ ...data, cached_at: Date.now() }, null, 2)
18408
+ );
18409
+ } catch {
18410
+ }
18411
+ }
18412
+ } catch {
18413
+ }
18414
+ }
18415
+ function estimateLocalCost(event) {
18416
+ if (FREE_SERVICES.has(event.service)) {
18417
+ return { type: "free" };
18418
+ }
18419
+ if (event.service.startsWith("unknown:")) {
18420
+ return { type: "unknown" };
18421
+ }
18422
+ if (!pricingData) {
18423
+ return { type: "loading" };
18424
+ }
18425
+ const svc = pricingData.services[event.service];
18426
+ if (!svc) {
18427
+ return { type: "unknown" };
18428
+ }
18429
+ if (svc.type === "llm") {
18430
+ if (!event.model) return { type: "unknown" };
18431
+ if (event.tokens_in === void 0 || event.tokens_out === void 0) {
18432
+ return { type: "no-tokens" };
18433
+ }
18434
+ let prices = svc.models[event.model];
18435
+ if (!prices) {
18436
+ const match = Object.keys(svc.models).find((m) => event.model.startsWith(m));
18437
+ if (match) prices = svc.models[match];
18438
+ }
18439
+ if (prices) {
18440
+ const inputCost = event.tokens_in / 1e6 * prices[0];
18441
+ const outputCost = event.tokens_out / 1e6 * prices[1];
18442
+ return { type: "priced", cost: inputCost + outputCost };
18443
+ }
18444
+ return { type: "unknown" };
18445
+ }
18446
+ if (svc.type === "api") {
18447
+ const endpoint = event.endpoint ?? "";
18448
+ for (const [prefix, cost] of Object.entries(svc.endpoints)) {
18449
+ if (prefix !== "*" && endpoint.startsWith(prefix)) {
18450
+ return cost === 0 ? { type: "free" } : { type: "priced", cost };
18451
+ }
18452
+ }
18453
+ const defaultCost = svc.endpoints["*"];
18454
+ if (defaultCost !== void 0) {
18455
+ return defaultCost === 0 ? { type: "free" } : { type: "priced", cost: defaultCost };
18456
+ }
18457
+ return { type: "unknown" };
18458
+ }
18459
+ if (svc.type === "fixed") {
18460
+ return { type: "fixed-tier" };
18461
+ }
18462
+ return { type: "unknown" };
18463
+ }
18464
+ var import_node_fs7, import_node_path8, pricingData, CACHE_FILE, CACHE_TTL_MS, FREE_SERVICES;
18465
+ var init_local_pricing = __esm({
18466
+ "src/transport/local-pricing.ts"() {
18467
+ "use strict";
18468
+ import_node_fs7 = __toESM(require("fs"));
18469
+ import_node_path8 = __toESM(require("path"));
18470
+ pricingData = null;
18471
+ CACHE_FILE = ".burn0/pricing-cache.json";
18472
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
18473
+ FREE_SERVICES = /* @__PURE__ */ new Set([
18474
+ "github-api",
18475
+ "slack-api",
18476
+ "discord-api"
18477
+ ]);
18478
+ }
18479
+ });
18480
+
18373
18481
  // src/cli/report.ts
18374
18482
  var report_exports = {};
18375
18483
  __export(report_exports, {
18484
+ aggregateLocal: () => aggregateLocal,
18376
18485
  runReport: () => runReport
18377
18486
  });
18378
- async function runReport() {
18379
- const cwd = process.cwd();
18380
- const ledger = new LocalLedger(cwd);
18381
- const events = ledger.read();
18382
- if (events.length === 0) {
18383
- console.log(source_default.dim("\n No data yet. Run your app with `import 'burn0'` to start tracking.\n"));
18384
- return;
18385
- }
18386
- const byService = {};
18487
+ function getLocalDateStr(date) {
18488
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
18489
+ }
18490
+ function formatCost(cost) {
18491
+ if (cost >= 1) return `$${cost.toFixed(2)}`;
18492
+ if (cost >= 0.01) return `$${cost.toFixed(4)}`;
18493
+ return `$${cost.toFixed(6)}`;
18494
+ }
18495
+ function formatDateLabel(dateStr) {
18496
+ const d = /* @__PURE__ */ new Date(dateStr + "T12:00:00");
18497
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
18498
+ return `${months[d.getMonth()]} ${String(d.getDate()).padStart(2, " ")}`;
18499
+ }
18500
+ function makeBar(value, max, width) {
18501
+ if (max === 0) return "\u2591".repeat(width);
18502
+ const filled = Math.round(value / max * width);
18503
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
18504
+ }
18505
+ function aggregateLocal(events, days) {
18506
+ const now = /* @__PURE__ */ new Date();
18507
+ const cutoffDate = new Date(now);
18508
+ cutoffDate.setDate(cutoffDate.getDate() - (days - 1));
18509
+ const cutoffStr = getLocalDateStr(cutoffDate);
18510
+ const serviceCosts = {};
18511
+ const serviceCallCounts = {};
18512
+ const dayCosts = {};
18513
+ let totalCost = 0;
18514
+ let totalCalls = 0;
18515
+ let unpricedCount = 0;
18516
+ let loadingCount = 0;
18387
18517
  for (const event of events) {
18388
- if (!byService[event.service]) {
18389
- byService[event.service] = { calls: 0, tokens_in: 0, tokens_out: 0 };
18518
+ const eventDate = new Date(event.timestamp);
18519
+ const eventDateStr = getLocalDateStr(eventDate);
18520
+ if (eventDateStr < cutoffStr) continue;
18521
+ totalCalls++;
18522
+ serviceCallCounts[event.service] = (serviceCallCounts[event.service] ?? 0) + 1;
18523
+ const estimate = estimateLocalCost(event);
18524
+ if (estimate.type === "priced" && estimate.cost > 0) {
18525
+ totalCost += estimate.cost;
18526
+ if (!serviceCosts[event.service]) serviceCosts[event.service] = { cost: 0, calls: 0 };
18527
+ serviceCosts[event.service].cost += estimate.cost;
18528
+ serviceCosts[event.service].calls++;
18529
+ if (!dayCosts[eventDateStr]) dayCosts[eventDateStr] = { cost: 0, calls: 0, services: {} };
18530
+ dayCosts[eventDateStr].cost += estimate.cost;
18531
+ dayCosts[eventDateStr].calls++;
18532
+ dayCosts[eventDateStr].services[event.service] = (dayCosts[eventDateStr].services[event.service] ?? 0) + estimate.cost;
18533
+ } else if (estimate.type === "free") {
18534
+ } else if (estimate.type === "loading") {
18535
+ loadingCount++;
18536
+ } else {
18537
+ unpricedCount++;
18390
18538
  }
18391
- byService[event.service].calls++;
18392
- byService[event.service].tokens_in += event.tokens_in ?? 0;
18393
- byService[event.service].tokens_out += event.tokens_out ?? 0;
18394
18539
  }
18395
- const sorted = Object.entries(byService).sort((a, b) => b[1].calls - a[1].calls);
18396
- const maxCalls = Math.max(...sorted.map(([, s]) => s.calls));
18397
- console.log(source_default.dim(`
18398
- Last 7 days: ${events.length} total calls
18540
+ const byService = Object.entries(serviceCosts).map(([name, data]) => ({ name, cost: data.cost, calls: data.calls })).sort((a, b) => b.cost - a.cost);
18541
+ const allServiceCalls = Object.entries(serviceCallCounts).map(([name, calls]) => ({ name, calls })).sort((a, b) => b.calls - a.calls);
18542
+ const byDay = Object.entries(dayCosts).sort((a, b) => b[0].localeCompare(a[0])).map(([date, data]) => {
18543
+ const topServices = Object.entries(data.services).sort((a, b) => b[1] - a[1]).map(([name, cost]) => ({ name, cost }));
18544
+ return { date, cost: data.cost, calls: data.calls, topServices };
18545
+ });
18546
+ return {
18547
+ total: { cost: totalCost, calls: totalCalls },
18548
+ byService,
18549
+ byDay,
18550
+ allServiceCalls,
18551
+ unpricedCount,
18552
+ pricingAvailable: loadingCount < totalCalls || totalCalls === 0
18553
+ };
18554
+ }
18555
+ function renderCallCountOnly(data) {
18556
+ const maxCalls = data.allServiceCalls.length > 0 ? data.allServiceCalls[0].calls : 0;
18557
+ const maxNameLen = Math.max(...data.allServiceCalls.map((s) => s.name.length), 8);
18558
+ for (const svc of data.allServiceCalls) {
18559
+ const bar = makeBar(svc.calls, maxCalls, 20);
18560
+ console.log(` ${svc.name.padEnd(maxNameLen)} ${source_default.gray(`${String(svc.calls).padStart(5)} calls`)} ${source_default.cyan(bar)}`);
18561
+ }
18562
+ console.log();
18563
+ }
18564
+ function renderCostReport(data, label, showDaily, isToday) {
18565
+ console.log(`
18566
+ ${source_default.hex("#FA5D19").bold("burn0 report")} ${source_default.gray(`\u2500\u2500 ${label}`)}
18567
+ `);
18568
+ if (data.total.calls === 0) {
18569
+ const msg = isToday ? "No calls today." : `No cost data yet. Run your app with \`import '@burn0/burn0'\` to start tracking.`;
18570
+ console.log(source_default.dim(` ${msg}
18571
+ `));
18572
+ return;
18573
+ }
18574
+ if (!data.pricingAvailable) {
18575
+ console.log(source_default.dim(` ${data.total.calls} calls tracked (pricing data not available)
18576
+ `));
18577
+ renderCallCountOnly(data);
18578
+ return;
18579
+ }
18580
+ if (data.total.cost === 0 && data.total.calls > 0) {
18581
+ console.log(source_default.dim(` ${data.total.calls} calls tracked (no pricing data available)
18399
18582
  `));
18400
- for (const [name, stats] of sorted) {
18401
- const barLength = Math.round(stats.calls / maxCalls * 20);
18402
- const bar = "\u2588".repeat(barLength) + "\u2591".repeat(20 - barLength);
18403
- const callsStr = `${stats.calls} calls`.padEnd(12);
18404
- console.log(` ${name.padEnd(16)} ${callsStr} ${source_default.cyan(bar)}`);
18583
+ renderCallCountOnly(data);
18584
+ return;
18585
+ }
18586
+ console.log(` ${source_default.bold("Total:")} ${source_default.green(formatCost(data.total.cost))} ${source_default.gray(`(${data.total.calls} calls)`)}
18587
+ `);
18588
+ const maxCost = data.byService.length > 0 ? data.byService[0].cost : 0;
18589
+ const maxNameLen = Math.max(...data.byService.map((s) => s.name.length), 8);
18590
+ for (const svc of data.byService) {
18591
+ const pct = data.total.cost > 0 ? Math.round(svc.cost / data.total.cost * 100) : 0;
18592
+ const bar = makeBar(svc.cost, maxCost, 20);
18593
+ console.log(` ${svc.name.padEnd(maxNameLen)} ${source_default.green(formatCost(svc.cost).padStart(10))} ${source_default.cyan(bar)} ${source_default.gray(`${String(pct).padStart(3)}%`)}`);
18594
+ }
18595
+ if (data.unpricedCount > 0) {
18596
+ console.log(source_default.dim(`
18597
+ + ${data.unpricedCount} calls not priced`));
18598
+ }
18599
+ if (showDaily && data.byDay.length > 0) {
18600
+ console.log(`
18601
+ ${source_default.gray("\u2500\u2500 daily \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
18602
+ `);
18603
+ const maxDayCost = Math.max(...data.byDay.map((d) => d.cost));
18604
+ for (const day of data.byDay) {
18605
+ const dateLabel = formatDateLabel(day.date);
18606
+ const bar = makeBar(day.cost, maxDayCost, 12);
18607
+ const top2 = day.topServices.slice(0, 2).map((s) => `${s.name} ${formatCost(s.cost)}`).join(" \xB7 ");
18608
+ const more = day.topServices.length > 2 ? ` +${day.topServices.length - 2} more` : "";
18609
+ console.log(` ${source_default.gray(dateLabel)} ${source_default.green(formatCost(day.cost).padStart(10))} ${source_default.cyan(bar)} ${source_default.dim(top2 + more)}`);
18610
+ }
18611
+ }
18612
+ if (data.total.cost > 0) {
18613
+ const daysInPeriod = showDaily ? 7 : 1;
18614
+ const dailyRate = data.total.cost / daysInPeriod;
18615
+ const monthly = dailyRate * 30;
18616
+ console.log(`
18617
+ ${source_default.gray("\u2500\u2500 projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}`);
18618
+ console.log(` ${source_default.gray("~")}${source_default.green(formatCost(monthly))}${source_default.gray("/mo estimated")} ${source_default.dim(`(based on ${isToday ? "today" : "last 7 days"})`)}`);
18405
18619
  }
18406
18620
  console.log();
18407
18621
  }
18622
+ async function fetchBackendReport(apiKey, days) {
18623
+ try {
18624
+ const controller = new AbortController();
18625
+ const timeout = setTimeout(() => controller.abort(), 5e3);
18626
+ const response = await globalThis.fetch(`${BURN0_API_URL}/v1/report?days=${days}`, {
18627
+ headers: { "Accept": "application/json", "Authorization": `Bearer ${apiKey}` },
18628
+ signal: controller.signal
18629
+ });
18630
+ clearTimeout(timeout);
18631
+ if (!response.ok) return null;
18632
+ const data = await response.json();
18633
+ return {
18634
+ total: data.total ?? { cost: 0, calls: 0 },
18635
+ byService: data.byService ?? [],
18636
+ byDay: data.byDay ?? [],
18637
+ allServiceCalls: (data.byService ?? []).map((s) => ({ name: s.name, calls: s.calls })),
18638
+ unpricedCount: 0,
18639
+ pricingAvailable: true
18640
+ };
18641
+ } catch {
18642
+ return null;
18643
+ }
18644
+ }
18645
+ async function runReport(options = {}) {
18646
+ const cwd = process.cwd();
18647
+ const days = options.today ? 1 : 7;
18648
+ const label = options.today ? "today" : "last 7 days";
18649
+ const apiKey = getApiKey();
18650
+ if (apiKey) {
18651
+ const backendData = await fetchBackendReport(apiKey, days);
18652
+ if (backendData) {
18653
+ renderCostReport(backendData, label, !options.today, !!options.today);
18654
+ return;
18655
+ }
18656
+ }
18657
+ await fetchPricing(BURN0_API_URL, globalThis.fetch);
18658
+ const ledger = new LocalLedger(cwd);
18659
+ const events = ledger.read();
18660
+ const data = aggregateLocal(events, days);
18661
+ renderCostReport(data, label, !options.today, !!options.today);
18662
+ }
18663
+ var BURN0_API_URL;
18408
18664
  var init_report = __esm({
18409
18665
  "src/cli/report.ts"() {
18410
18666
  "use strict";
18411
18667
  init_source();
18412
18668
  init_local();
18669
+ init_local_pricing();
18670
+ init_env();
18671
+ BURN0_API_URL = process.env.BURN0_API_URL ?? "https://api.burn0.dev";
18413
18672
  }
18414
18673
  });
18415
18674
 
@@ -18423,29 +18682,29 @@ async function runDev(command) {
18423
18682
  console.log("\n Usage: burn0 dev -- node app.js\n");
18424
18683
  process.exit(1);
18425
18684
  }
18426
- const registerPath = import_node_path8.default.resolve(__dirname, "../register.js");
18685
+ const registerPath = import_node_path9.default.resolve(__dirname, "../register.js");
18427
18686
  const [cmd, ...args] = command;
18428
18687
  if (cmd === "node") {
18429
- const child = (0, import_node_child_process.spawn)(cmd, ["--require", registerPath, ...args], {
18688
+ const child = (0, import_node_child_process2.spawn)(cmd, ["--require", registerPath, ...args], {
18430
18689
  stdio: "inherit",
18431
18690
  env: { ...process.env }
18432
18691
  });
18433
18692
  child.on("exit", (code) => process.exit(code ?? 0));
18434
18693
  } else {
18435
18694
  const nodeOptions = `--require ${registerPath} ${process.env.NODE_OPTIONS ?? ""}`;
18436
- const child = (0, import_node_child_process.spawn)(cmd, args, {
18695
+ const child = (0, import_node_child_process2.spawn)(cmd, args, {
18437
18696
  stdio: "inherit",
18438
18697
  env: { ...process.env, NODE_OPTIONS: nodeOptions.trim() }
18439
18698
  });
18440
18699
  child.on("exit", (code) => process.exit(code ?? 0));
18441
18700
  }
18442
18701
  }
18443
- var import_node_child_process, import_node_path8;
18702
+ var import_node_child_process2, import_node_path9;
18444
18703
  var init_dev = __esm({
18445
18704
  "src/cli/dev.ts"() {
18446
18705
  "use strict";
18447
- import_node_child_process = require("child_process");
18448
- import_node_path8 = __toESM(require("path"));
18706
+ import_node_child_process2 = require("child_process");
18707
+ import_node_path9 = __toESM(require("path"));
18449
18708
  }
18450
18709
  });
18451
18710
 
@@ -18481,9 +18740,9 @@ program2.command("connect").description("Add your burn0 API key").action(async (
18481
18740
  const { runConnect: runConnect2 } = await Promise.resolve().then(() => (init_connect(), connect_exports));
18482
18741
  await runConnect2();
18483
18742
  });
18484
- program2.command("report").description("Show cost summary").action(async () => {
18743
+ program2.command("report").description("Show cost summary").option("--today", "Show today only").action(async (options) => {
18485
18744
  const { runReport: runReport2 } = await Promise.resolve().then(() => (init_report(), report_exports));
18486
- await runReport2();
18745
+ await runReport2(options);
18487
18746
  });
18488
18747
  program2.command("dev").description("Run your app with burn0 cost tracking").argument("[command...]", "Command to run").passThroughOptions().allowUnknownOption().action(async (command) => {
18489
18748
  const { runDev: runDev2 } = await Promise.resolve().then(() => (init_dev(), dev_exports));