aiopt 0.3.2 → 0.3.4

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.js CHANGED
@@ -656,7 +656,7 @@ var require_package = __commonJS({
656
656
  "package.json"(exports2, module2) {
657
657
  module2.exports = {
658
658
  name: "aiopt",
659
- version: "0.3.2",
659
+ version: "0.3.4",
660
660
  description: "Pre-deploy LLM cost accident guardrail (serverless local CLI)",
661
661
  bin: {
662
662
  aiopt: "dist/cli.js"
@@ -1667,10 +1667,6 @@ var init_guard = __esm({
1667
1667
  });
1668
1668
 
1669
1669
  // src/collect.ts
1670
- var collect_exports = {};
1671
- __export(collect_exports, {
1672
- collectToUsageJsonl: () => collectToUsageJsonl
1673
- });
1674
1670
  function exists(p) {
1675
1671
  try {
1676
1672
  return import_fs10.default.existsSync(p);
@@ -1819,13 +1815,17 @@ async function startDashboard(cwd, opts) {
1819
1815
  const port = opts.port || 3010;
1820
1816
  const outDir = import_path12.default.join(cwd, "aiopt-output");
1821
1817
  const file = (name) => import_path12.default.join(outDir, name);
1818
+ let lastCollect = null;
1819
+ let lastCollectError = null;
1822
1820
  function ensureUsageFile() {
1823
1821
  try {
1824
1822
  const usagePath = file("usage.jsonl");
1825
1823
  if (import_fs11.default.existsSync(usagePath)) return;
1826
- const { collectToUsageJsonl: collectToUsageJsonl2 } = (init_collect(), __toCommonJS(collect_exports));
1827
- collectToUsageJsonl2(usagePath);
1828
- } catch {
1824
+ const r = collectToUsageJsonl(usagePath);
1825
+ lastCollect = { ts: (/* @__PURE__ */ new Date()).toISOString(), outPath: r.outPath, sources: r.sources, eventsWritten: r.eventsWritten };
1826
+ lastCollectError = null;
1827
+ } catch (e) {
1828
+ lastCollectError = String(e?.message || e || "collect failed");
1829
1829
  }
1830
1830
  }
1831
1831
  ensureUsageFile();
@@ -1837,6 +1837,14 @@ async function startDashboard(cwd, opts) {
1837
1837
  return null;
1838
1838
  }
1839
1839
  }
1840
+ function statOrNull(p) {
1841
+ try {
1842
+ const st = import_fs11.default.statSync(p);
1843
+ return { size: st.size, mtimeMs: st.mtimeMs };
1844
+ } catch {
1845
+ return null;
1846
+ }
1847
+ }
1840
1848
  const indexHtml = `<!doctype html>
1841
1849
  <html lang="en">
1842
1850
  <head>
@@ -2096,114 +2104,75 @@ async function load(){
2096
2104
  document.getElementById('scanMeta').textContent = '(no report.json yet \u2014 run: aiopt scan)';
2097
2105
  }
2098
2106
 
2099
- const usageTxt = await fetch('/api/usage.jsonl', { cache: 'no-store' }).then(r=>r.ok?r.text():null).catch(()=>null);
2100
- if(usageTxt){
2101
- const now = Date.now();
2102
-
2103
- // Live: last 60m (per-minute cost + calls)
2104
- const liveBins = Array.from({length:60}, (_,i)=>({ min:i, cost:0, calls:0 }));
2105
-
2106
- // 7d cost trend: sum(cost_usd) per day from usage.jsonl (ev.ts).
2107
- const bins = Array.from({length:7}, (_,i)=>({ day:i, cost:0, calls:0 }));
2108
-
2109
- for(const line of usageTxt.trim().split('
2110
- ')){
2111
- if(!line) continue;
2112
- try{
2113
- const ev = JSON.parse(line);
2114
- const t = Date.parse(ev.ts);
2115
- if(!Number.isFinite(t)) continue;
2116
-
2117
- const cost = Number(ev.cost_usd);
2118
- const c = Number.isFinite(cost) ? cost : 0;
2119
-
2120
- // live minute bin
2121
- const dm = Math.floor((now - t) / 60000);
2122
- if(dm>=0 && dm<60){
2123
- liveBins[dm].calls++;
2124
- liveBins[dm].cost += c;
2125
- }
2126
-
2127
- // 7d day bin
2128
- const d = Math.floor((now - t) / 86400000);
2129
- if(d>=0 && d<7){
2130
- bins[d].calls++;
2131
- bins[d].cost += c;
2132
- }
2133
- }catch{}
2134
- }
2135
-
2136
- // Live sparkline (last 60m)
2137
- {
2138
- const W=520, H=120, P=12;
2139
- const pts = liveBins.slice().reverse();
2140
- const max = Math.max(...pts.map(b=>b.cost), 0.000001);
2141
- const xs = pts.map((_,i)=> P + (i*(W-2*P))/59);
2142
- const ys = pts.map(b=> H-P - ((b.cost/max)*(H-2*P)) );
2143
- let d = '';
2144
- for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
2145
- const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
2146
- const svg =
2147
- '<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
2148
- '<path d="'+area+'" fill="rgba(167,139,250,.10)" />'+
2149
- '<path d="'+d+'" fill="none" stroke="rgba(167,139,250,.95)" stroke-width="2" />'+
2150
- '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max/min '+money(max)+'</text>'+
2151
- '</svg>';
2152
- document.getElementById('liveSvg').innerHTML = svg;
2153
-
2154
- const rows = pts.slice(-10).map((b,idx)=>{
2155
- const mAgo = 9-idx;
2156
- const label = (mAgo===0 ? 'now' : (mAgo+'m'));
2157
- const dollars = ('$' + (Math.round(b.cost*100)/100).toFixed(2));
2158
- return String(label).padEnd(5) + ' ' + String(dollars).padStart(9) + ' (' + b.calls + ' calls)';
2159
- });
2160
- document.getElementById('liveText').textContent = rows.join('
2107
+ // Use computed JSON endpoints to avoid downloading/parsing huge usage.jsonl in the browser.
2108
+ const live60 = await fetch('/api/live-60m.json', { cache: 'no-store' }).then(r=>r.ok?r.json():null).catch(()=>null);
2109
+ const sum7d = await fetch('/api/usage-summary.json', { cache: 'no-store' }).then(r=>r.ok?r.json():null).catch(()=>null);
2110
+
2111
+ if(live60 && live60.bins){
2112
+ const pts = (live60.bins || []).slice().reverse();
2113
+ const W=520, H=120, P=12;
2114
+ const max = Math.max(...pts.map(b=>Number(b.cost)||0), 0.000001);
2115
+ const xs = pts.map((_,i)=> P + (i*(W-2*P))/59);
2116
+ const ys = pts.map(b=> H-P - (((Number(b.cost)||0)/max)*(H-2*P)) );
2117
+ let d = '';
2118
+ for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
2119
+ const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
2120
+ const svg =
2121
+ '<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
2122
+ '<path d="'+area+'" fill="rgba(167,139,250,.10)" />'+
2123
+ '<path d="'+d+'" fill="none" stroke="rgba(167,139,250,.95)" stroke-width="2" />'+
2124
+ '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max/min '+money(max)+'</text>'+
2125
+ '</svg>';
2126
+ document.getElementById('liveSvg').innerHTML = svg;
2127
+
2128
+ const rows = pts.slice(-10).map((b,idx)=>{
2129
+ const mAgo = 9-idx;
2130
+ const label = (mAgo===0 ? 'now' : (mAgo+'m'));
2131
+ const dollars = ('$' + (Math.round((Number(b.cost)||0)*100)/100).toFixed(2));
2132
+ return String(label).padEnd(5) + ' ' + String(dollars).padStart(9) + ' (' + (b.calls||0) + ' calls)';
2133
+ });
2134
+ document.getElementById('liveText').textContent = rows.join('
2161
2135
  ');
2162
2136
 
2163
- const totalLive = pts.reduce((a,b)=>a+b.cost,0);
2164
- const liveEl = document.getElementById('live');
2165
- if(liveEl){
2166
- liveEl.textContent = 'live: on \xB7 last60m ' + money(totalLive);
2167
- }
2137
+ const liveEl = document.getElementById('live');
2138
+ if(liveEl){
2139
+ liveEl.textContent = 'live: on \xB7 last60m ' + money(live60.totalCostUsd || 0);
2168
2140
  }
2141
+ } else {
2142
+ document.getElementById('liveText').textContent = '(no live data yet)';
2143
+ document.getElementById('liveSvg').innerHTML = '';
2144
+ }
2169
2145
 
2170
- // 7d sparkline
2171
- {
2172
- const W=520, H=120, P=12;
2173
- const pts = bins.slice().reverse();
2174
- const max = Math.max(...pts.map(b=>b.cost), 0.000001);
2175
- const xs = pts.map((_,i)=> P + (i*(W-2*P))/6);
2176
- const ys = pts.map(b=> H-P - ((b.cost/max)*(H-2*P)) );
2177
- let d = '';
2178
- for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
2179
- const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
2180
-
2181
- const circles = xs.map((x,i)=>'<circle cx="'+x.toFixed(1)+'" cy="'+ys[i].toFixed(1)+'" r="2.6" fill="rgba(52,211,153,.9)"/>').join('');
2182
- const svg =
2183
- '<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
2184
- '<path d="'+area+'" fill="rgba(96,165,250,.12)" />'+
2185
- '<path d="'+d+'" fill="none" stroke="rgba(96,165,250,.95)" stroke-width="2" />'+
2186
- circles+
2187
- '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max '+money(max)+'</text>'+
2188
- '</svg>';
2189
- document.getElementById('trendSvg').innerHTML = svg;
2190
-
2191
- const rows = pts.map((b,idx)=>{
2192
- const label = (idx===pts.length-1 ? 'd-6' : (idx===0 ? 'today' : ('d-'+idx)));
2193
- const dollars = ('$' + (Math.round(b.cost*100)/100).toFixed(2));
2194
- return String(label).padEnd(7) + ' ' + String(dollars).padStart(9) + ' (' + b.calls + ' calls)';
2195
- });
2196
- document.getElementById('trend').textContent = rows.join('
2146
+ if(sum7d && sum7d.dayBins){
2147
+ const bins = sum7d.dayBins || [];
2148
+ const W=520, H=120, P=12;
2149
+ const pts = bins.slice().reverse();
2150
+ const max = Math.max(...pts.map(b=>Number(b.cost)||0), 0.000001);
2151
+ const xs = pts.map((_,i)=> P + (i*(W-2*P))/6);
2152
+ const ys = pts.map(b=> H-P - (((Number(b.cost)||0)/max)*(H-2*P)) );
2153
+ let d = '';
2154
+ for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
2155
+ const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
2156
+ const circles = xs.map((x,i)=>'<circle cx="'+x.toFixed(1)+'" cy="'+ys[i].toFixed(1)+'" r="2.6" fill="rgba(52,211,153,.9)"/>').join('');
2157
+ const svg =
2158
+ '<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" xmlns="http://www.w3.org/2000/svg" style="background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.10); border-radius:14px">'+
2159
+ '<path d="'+area+'" fill="rgba(96,165,250,.12)" />'+
2160
+ '<path d="'+d+'" fill="none" stroke="rgba(96,165,250,.95)" stroke-width="2" />'+
2161
+ circles+
2162
+ '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max '+money(max)+'</text>'+
2163
+ '</svg>';
2164
+ document.getElementById('trendSvg').innerHTML = svg;
2165
+
2166
+ const rows = pts.map((b,idx)=>{
2167
+ const label = (idx===pts.length-1 ? 'd-6' : (idx===0 ? 'today' : ('d-'+idx)));
2168
+ const dollars = ('$' + (Math.round((Number(b.cost)||0)*100)/100).toFixed(2));
2169
+ return String(label).padEnd(7) + ' ' + String(dollars).padStart(9) + ' (' + (b.calls||0) + ' calls)';
2170
+ });
2171
+ document.getElementById('trend').textContent = rows.join('
2197
2172
  ');
2198
- }
2199
-
2200
2173
  } else {
2201
- document.getElementById('liveText').textContent = '(no usage.jsonl yet)';
2202
- document.getElementById('liveSvg').innerHTML = '';
2203
- document.getElementById('trend').textContent = '(no usage.jsonl yet)';
2174
+ document.getElementById('trend').textContent = '(no 7d data yet)';
2204
2175
  document.getElementById('trendSvg').innerHTML = '';
2205
- const liveEl = document.getElementById('live');
2206
- if(liveEl) liveEl.textContent = 'live: off';
2207
2176
  }
2208
2177
 
2209
2178
  const reportMd = await fetch('/api/report.md').then(r=>r.ok?r.text():null).catch(()=>null);
@@ -2232,16 +2201,77 @@ if(liveEl) liveEl.textContent = 'live: on (polling)';
2232
2201
  const expected = ["guard-last.txt", "guard-last.json", "report.json", "report.md", "usage.jsonl", "guard-history.jsonl"];
2233
2202
  const missing = expected.filter((f) => !import_fs11.default.existsSync(file(f)));
2234
2203
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
2235
- res.end(JSON.stringify({ baseDir: cwd, outDir, missing }, null, 2));
2204
+ res.end(JSON.stringify({ baseDir: cwd, outDir, missing, collect: lastCollect, collectError: lastCollectError }, null, 2));
2236
2205
  return;
2237
2206
  }
2238
- const allow = /* @__PURE__ */ new Set(["guard-last.txt", "guard-last.json", "guard-history.jsonl", "report.md", "report.json", "usage.jsonl"]);
2239
- if (name === "usage.jsonl") ensureUsageFile();
2207
+ const allow = /* @__PURE__ */ new Set([
2208
+ "guard-last.txt",
2209
+ "guard-last.json",
2210
+ "guard-history.jsonl",
2211
+ "report.md",
2212
+ "report.json",
2213
+ "usage.jsonl",
2214
+ "usage-summary.json",
2215
+ "live-60m.json"
2216
+ ]);
2217
+ if (name === "usage.jsonl" || name === "usage-summary.json" || name === "live-60m.json") ensureUsageFile();
2240
2218
  if (!allow.has(name)) {
2241
2219
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
2242
2220
  res.end("not found");
2243
2221
  return;
2244
2222
  }
2223
+ if (name === "live-60m.json" || name === "usage-summary.json") {
2224
+ const usagePath = file("usage.jsonl");
2225
+ const st = statOrNull(usagePath);
2226
+ if (!st) {
2227
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
2228
+ res.end("missing");
2229
+ return;
2230
+ }
2231
+ globalThis.__aioptDashCache = globalThis.__aioptDashCache || {};
2232
+ const cache = globalThis.__aioptDashCache;
2233
+ if (!cache.usage || cache.usage.mtimeMs !== st.mtimeMs) {
2234
+ const txt2 = import_fs11.default.readFileSync(usagePath, "utf8");
2235
+ const now = Date.now();
2236
+ const liveBins = Array.from({ length: 60 }, () => ({ cost: 0, calls: 0 }));
2237
+ const dayBins = Array.from({ length: 7 }, () => ({ cost: 0, calls: 0 }));
2238
+ for (const line of txt2.split(/\r?\n/)) {
2239
+ if (!line.trim()) continue;
2240
+ try {
2241
+ const ev = JSON.parse(line);
2242
+ const t = Date.parse(ev.ts);
2243
+ if (!Number.isFinite(t)) continue;
2244
+ const cost = Number(ev.cost_usd);
2245
+ const c = Number.isFinite(cost) ? cost : 0;
2246
+ const dm = Math.floor((now - t) / 6e4);
2247
+ if (dm >= 0 && dm < 60) {
2248
+ liveBins[dm].calls += 1;
2249
+ liveBins[dm].cost += c;
2250
+ }
2251
+ const dd = Math.floor((now - t) / 864e5);
2252
+ if (dd >= 0 && dd < 7) {
2253
+ dayBins[dd].calls += 1;
2254
+ dayBins[dd].cost += c;
2255
+ }
2256
+ } catch {
2257
+ }
2258
+ }
2259
+ cache.usage = {
2260
+ mtimeMs: st.mtimeMs,
2261
+ live60m: {
2262
+ bins: liveBins,
2263
+ totalCostUsd: Math.round(liveBins.reduce((a, b) => a + b.cost, 0) * 100) / 100
2264
+ },
2265
+ summary7d: {
2266
+ dayBins
2267
+ }
2268
+ };
2269
+ }
2270
+ const body = name === "live-60m.json" ? cache.usage.live60m : cache.usage.summary7d;
2271
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
2272
+ res.end(JSON.stringify(body, null, 2));
2273
+ return;
2274
+ }
2245
2275
  const p = file(name);
2246
2276
  const txt = readOrNull(p);
2247
2277
  if (txt === null) {
@@ -2250,7 +2280,7 @@ if(liveEl) liveEl.textContent = 'live: on (polling)';
2250
2280
  return;
2251
2281
  }
2252
2282
  const ct = name.endsWith(".json") ? "application/json; charset=utf-8" : "text/plain; charset=utf-8";
2253
- res.writeHead(200, { "content-type": ct });
2283
+ res.writeHead(200, { "content-type": ct, "cache-control": "no-store" });
2254
2284
  res.end(txt);
2255
2285
  return;
2256
2286
  }
@@ -2273,6 +2303,7 @@ var init_dashboard = __esm({
2273
2303
  import_http = __toESM(require("http"));
2274
2304
  import_fs11 = __toESM(require("fs"));
2275
2305
  import_path12 = __toESM(require("path"));
2306
+ init_collect();
2276
2307
  }
2277
2308
  });
2278
2309