aiopt 0.3.3 → 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.3",
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"
@@ -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);
@@ -2235,13 +2204,74 @@ if(liveEl) liveEl.textContent = 'live: on (polling)';
2235
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
  }