aiopt 0.3.1 → 0.3.2

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.1",
659
+ version: "0.3.2",
660
660
  description: "Pre-deploy LLM cost accident guardrail (serverless local CLI)",
661
661
  bin: {
662
662
  aiopt: "dist/cli.js"
@@ -1666,6 +1666,149 @@ var init_guard = __esm({
1666
1666
  }
1667
1667
  });
1668
1668
 
1669
+ // src/collect.ts
1670
+ var collect_exports = {};
1671
+ __export(collect_exports, {
1672
+ collectToUsageJsonl: () => collectToUsageJsonl
1673
+ });
1674
+ function exists(p) {
1675
+ try {
1676
+ return import_fs10.default.existsSync(p);
1677
+ } catch {
1678
+ return false;
1679
+ }
1680
+ }
1681
+ function safeReadJsonl(p) {
1682
+ const out = [];
1683
+ try {
1684
+ const txt = import_fs10.default.readFileSync(p, "utf8");
1685
+ for (const line of txt.split(/\r?\n/)) {
1686
+ if (!line.trim()) continue;
1687
+ try {
1688
+ out.push(JSON.parse(line));
1689
+ } catch {
1690
+ }
1691
+ }
1692
+ } catch {
1693
+ }
1694
+ return out;
1695
+ }
1696
+ function listJsonlFiles(dir) {
1697
+ const out = [];
1698
+ try {
1699
+ for (const name of import_fs10.default.readdirSync(dir)) {
1700
+ const full = import_path11.default.join(dir, name);
1701
+ let st;
1702
+ try {
1703
+ st = import_fs10.default.statSync(full);
1704
+ } catch {
1705
+ continue;
1706
+ }
1707
+ if (st.isFile() && name.endsWith(".jsonl")) out.push(full);
1708
+ }
1709
+ } catch {
1710
+ }
1711
+ return out;
1712
+ }
1713
+ function findOpenClawSessionLogs() {
1714
+ const home = import_os2.default.homedir();
1715
+ const root = import_path11.default.join(home, ".openclaw", "agents");
1716
+ if (!exists(root)) return [];
1717
+ const found = [];
1718
+ let agents = [];
1719
+ try {
1720
+ agents = import_fs10.default.readdirSync(root);
1721
+ } catch {
1722
+ agents = [];
1723
+ }
1724
+ for (const a of agents) {
1725
+ const sessDir = import_path11.default.join(root, a, "sessions");
1726
+ if (!exists(sessDir)) continue;
1727
+ for (const f of listJsonlFiles(sessDir)) found.push(f);
1728
+ }
1729
+ return found;
1730
+ }
1731
+ function parseOpenClawSessionFile(p) {
1732
+ const rows = safeReadJsonl(p);
1733
+ const events = [];
1734
+ for (const r of rows) {
1735
+ if (r && r.type === "message" && r.message && typeof r.message === "object") {
1736
+ const m = r.message;
1737
+ const u = m.usage;
1738
+ if (!u) continue;
1739
+ const input = Number(u.input ?? u.prompt ?? u.prompt_tokens ?? 0);
1740
+ const output = Number(u.output ?? u.completion ?? u.completion_tokens ?? 0);
1741
+ const costTotal = u.cost && typeof u.cost === "object" ? Number(u.cost.total ?? u.costTotal ?? u.cost_usd) : void 0;
1742
+ let tsRaw = m.timestamp || r.timestamp || (/* @__PURE__ */ new Date()).toISOString();
1743
+ let ts = String(tsRaw);
1744
+ if (String(tsRaw).match(/^\d{10,}$/)) {
1745
+ try {
1746
+ ts = new Date(Number(tsRaw)).toISOString();
1747
+ } catch {
1748
+ }
1749
+ }
1750
+ const provider = String(m.provider || r.provider || "openclaw");
1751
+ const model = String(m.model || r.modelId || "unknown");
1752
+ if (!Number.isFinite(input) && !Number.isFinite(output) && !Number.isFinite(costTotal)) continue;
1753
+ events.push({
1754
+ ts,
1755
+ provider,
1756
+ model,
1757
+ input_tokens: Number.isFinite(input) ? input : 0,
1758
+ output_tokens: Number.isFinite(output) ? output : 0,
1759
+ retries: 0,
1760
+ status: "ok",
1761
+ cost_usd: Number.isFinite(costTotal) ? Number(costTotal) : void 0,
1762
+ meta: {
1763
+ source: "openclaw-session",
1764
+ session_file: p,
1765
+ cache_read_tokens: u.cacheRead,
1766
+ cache_write_tokens: u.cacheWrite,
1767
+ total_tokens: u.totalTokens
1768
+ }
1769
+ });
1770
+ }
1771
+ }
1772
+ return events;
1773
+ }
1774
+ function stableKey(e) {
1775
+ return `${e.ts}|${e.provider}|${e.model}|${e.input_tokens}|${e.output_tokens}|${e.cost_usd ?? ""}`;
1776
+ }
1777
+ function collectToUsageJsonl(outPath) {
1778
+ const all = [];
1779
+ const sources = [];
1780
+ const ocFiles = findOpenClawSessionLogs();
1781
+ let ocEvents = 0;
1782
+ for (const f of ocFiles) {
1783
+ const evs = parseOpenClawSessionFile(f);
1784
+ ocEvents += evs.length;
1785
+ all.push(...evs);
1786
+ }
1787
+ sources.push({ name: "openclaw", files: ocFiles.length, events: ocEvents });
1788
+ const seen = /* @__PURE__ */ new Set();
1789
+ const uniq = [];
1790
+ for (const e of all) {
1791
+ const k = stableKey(e);
1792
+ if (seen.has(k)) continue;
1793
+ seen.add(k);
1794
+ uniq.push(e);
1795
+ }
1796
+ uniq.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
1797
+ import_fs10.default.mkdirSync(import_path11.default.dirname(outPath), { recursive: true });
1798
+ const lines = uniq.map((e) => JSON.stringify(e)).join("\n") + (uniq.length ? "\n" : "");
1799
+ import_fs10.default.writeFileSync(outPath, lines);
1800
+ return { outPath, sources, eventsWritten: uniq.length };
1801
+ }
1802
+ var import_fs10, import_path11, import_os2;
1803
+ var init_collect = __esm({
1804
+ "src/collect.ts"() {
1805
+ "use strict";
1806
+ import_fs10 = __toESM(require("fs"));
1807
+ import_path11 = __toESM(require("path"));
1808
+ import_os2 = __toESM(require("os"));
1809
+ }
1810
+ });
1811
+
1669
1812
  // src/dashboard.ts
1670
1813
  var dashboard_exports = {};
1671
1814
  __export(dashboard_exports, {
@@ -1674,12 +1817,22 @@ __export(dashboard_exports, {
1674
1817
  async function startDashboard(cwd, opts) {
1675
1818
  const host = "127.0.0.1";
1676
1819
  const port = opts.port || 3010;
1677
- const outDir = import_path11.default.join(cwd, "aiopt-output");
1678
- const file = (name) => import_path11.default.join(outDir, name);
1820
+ const outDir = import_path12.default.join(cwd, "aiopt-output");
1821
+ const file = (name) => import_path12.default.join(outDir, name);
1822
+ function ensureUsageFile() {
1823
+ try {
1824
+ const usagePath = file("usage.jsonl");
1825
+ if (import_fs11.default.existsSync(usagePath)) return;
1826
+ const { collectToUsageJsonl: collectToUsageJsonl2 } = (init_collect(), __toCommonJS(collect_exports));
1827
+ collectToUsageJsonl2(usagePath);
1828
+ } catch {
1829
+ }
1830
+ }
1831
+ ensureUsageFile();
1679
1832
  function readOrNull(p) {
1680
1833
  try {
1681
- if (!import_fs10.default.existsSync(p)) return null;
1682
- return import_fs10.default.readFileSync(p, "utf8");
1834
+ if (!import_fs11.default.existsSync(p)) return null;
1835
+ return import_fs11.default.readFileSync(p, "utf8");
1683
1836
  } catch {
1684
1837
  return null;
1685
1838
  }
@@ -1751,8 +1904,9 @@ async function startDashboard(cwd, opts) {
1751
1904
  <div>
1752
1905
  <div class="h1">AIOpt Local Dashboard</div>
1753
1906
  <div class="mini" id="baseDir">base: \u2014</div>
1907
+ <div class="mini" id="missingHint" style="margin-top:4px">checking files\u2026</div>
1754
1908
  </div>
1755
- <div class="pill"><span class="dot"></span> local-only \xB7 reads <span class="k">./aiopt-output</span></div>
1909
+ <div class="pill"><span class="dot"></span> local-only \xB7 reads <span class="k">./aiopt-output</span> \xB7 <span id="live" class="muted">live: off</span></div>
1756
1910
  </div>
1757
1911
 
1758
1912
  <div class="grid">
@@ -1764,7 +1918,7 @@ async function startDashboard(cwd, opts) {
1764
1918
  <div style="height:8px"></div>
1765
1919
  <div id="guardBadge" class="badge"><span class="b"></span><span id="guardBadgeText">loading\u2026</span></div>
1766
1920
  <div style="height:10px"></div>
1767
- <pre id="guard">loading\u2026</pre>
1921
+ <pre id="guard">loading\u2026 (if this stays, it usually means required files are missing \u2014 see the top \u201Cmissing\u201D line)</pre>
1768
1922
  <div style="height:10px"></div>
1769
1923
  <div class="row">
1770
1924
  <a href="/api/guard-last.txt" target="_blank">raw txt</a>
@@ -1777,6 +1931,14 @@ async function startDashboard(cwd, opts) {
1777
1931
  <div style="height:12px"></div>
1778
1932
  <div style="font-weight:900">Recent guard runs</div>
1779
1933
  <pre id="guardHist" style="max-height:220px; overflow:auto">loading\u2026</pre>
1934
+ <div class="mini" style="margin-top:8px">
1935
+ Quick actions (copy/paste):
1936
+ <div style="margin-top:6px; display:flex; flex-wrap:wrap; gap:8px">
1937
+ <span class="k">aiopt quickstart --demo</span>
1938
+ <span class="k">aiopt guard --help</span>
1939
+ <span class="k">aiopt scan</span>
1940
+ </div>
1941
+ </div>
1780
1942
  </div>
1781
1943
 
1782
1944
  <div class="card c6">
@@ -1792,9 +1954,17 @@ async function startDashboard(cwd, opts) {
1792
1954
 
1793
1955
  <div style="height:12px"></div>
1794
1956
  <div class="row" style="justify-content:space-between">
1795
- <div style="font-weight:900">Cost trend (last 7d)</div>
1957
+ <div style="font-weight:900">Live usage (last 60m)</div>
1796
1958
  <div class="mini"><a href="/api/usage.jsonl" target="_blank">usage.jsonl</a></div>
1797
1959
  </div>
1960
+ <div id="liveSvg" style="margin-top:8px"></div>
1961
+ <pre id="liveText" style="margin-top:8px">loading\u2026</pre>
1962
+
1963
+ <div style="height:12px"></div>
1964
+ <div class="row" style="justify-content:space-between">
1965
+ <div style="font-weight:900">Cost trend (last 7d)</div>
1966
+ <div class="mini">(from usage.jsonl)</div>
1967
+ </div>
1798
1968
  <div id="trendSvg" style="margin-top:8px"></div>
1799
1969
  <pre id="trend" style="margin-top:8px">loading\u2026</pre>
1800
1970
 
@@ -1844,19 +2014,41 @@ function renderBars(el, items){
1844
2014
  }
1845
2015
  }
1846
2016
 
2017
+ let __live = false;
2018
+ let __tick = 0;
2019
+
1847
2020
  async function load(){
1848
- const meta = await fetch('/api/_meta').then(r=>r.ok?r.json():null);
2021
+ __tick++;
2022
+ // If fetch hangs / fails, do not leave \u201Cloading\u2026\u201D forever.
2023
+ const timer = setTimeout(()=>{
2024
+ const el = document.getElementById('missingHint');
2025
+ if(el && el.textContent && el.textContent.includes('checking')){
2026
+ el.textContent = 'still loading\u2026 (if this doesn't change, refresh. If it persists: run aiopt quickstart --demo or aiopt scan)';
2027
+ }
2028
+ }, 1500);
2029
+
2030
+ let meta = null;
2031
+ try{
2032
+ meta = await fetch('/api/_meta', { cache: 'no-store' }).then(r=>r.ok?r.json():null);
2033
+ }catch{}
2034
+ clearTimeout(timer);
2035
+
1849
2036
  if(meta && meta.baseDir){
1850
2037
  document.getElementById('baseDir').textContent = 'base: ' + meta.baseDir;
1851
2038
  }
1852
- if(meta && meta.missing && meta.missing.length){
1853
- // show missing files hint in the guard panel if nothing else yet
1854
- // (prevents users from thinking it is stuck on loading)
1855
- document.getElementById('guard').textContent = '(missing: ' + meta.missing.join(', ') + ')';
2039
+
2040
+ const miss = (meta && meta.missing) ? meta.missing : null;
2041
+ const hint = document.getElementById('missingHint');
2042
+ if(miss && miss.length){
2043
+ hint.textContent = 'missing: ' + miss.join(', ') + ' \u2192 not broken. Run: aiopt quickstart --demo (or aiopt scan)';
2044
+ } else if(miss && miss.length===0){
2045
+ hint.textContent = 'missing: (none)';
2046
+ } else {
2047
+ hint.textContent = 'missing: (unknown \u2014 failed to load /api/_meta)';
1856
2048
  }
1857
2049
 
1858
- const guardTxt = await fetch('/api/guard-last.txt').then(r=>r.ok?r.text():null);
1859
- const guardMeta = await fetch('/api/guard-last.json').then(r=>r.ok?r.json():null);
2050
+ const guardTxt = await fetch('/api/guard-last.txt', { cache: 'no-store' }).then(r=>r.ok?r.text():null).catch(()=>null);
2051
+ const guardMeta = await fetch('/api/guard-last.json', { cache: 'no-store' }).then(r=>r.ok?r.json():null).catch(()=>null);
1860
2052
 
1861
2053
  document.getElementById('guard').textContent = guardTxt || '(no guard-last.txt yet \u2014 run: aiopt guard)';
1862
2054
  if(guardMeta){
@@ -1870,7 +2062,7 @@ async function load(){
1870
2062
  else {badge.classList.add('fail'); t.textContent='FAIL (3)';}
1871
2063
  }
1872
2064
 
1873
- const histTxt = await fetch('/api/guard-history.jsonl').then(r=>r.ok?r.text():null);
2065
+ const histTxt = await fetch('/api/guard-history.jsonl', { cache: 'no-store' }).then(r=>r.ok?r.text():null).catch(()=>null);
1874
2066
  if(histTxt){
1875
2067
  const lines = histTxt.trim().split('
1876
2068
  ').filter(Boolean).slice(-15).reverse();
@@ -1891,7 +2083,7 @@ async function load(){
1891
2083
  document.getElementById('guardHist').textContent = '(no guard-history.jsonl yet \u2014 run: aiopt guard)';
1892
2084
  }
1893
2085
 
1894
- const reportJson = await fetch('/api/report.json').then(r=>r.ok?r.json():null);
2086
+ const reportJson = await fetch('/api/report.json', { cache: 'no-store' }).then(r=>r.ok?r.json():null).catch(()=>null);
1895
2087
  if(reportJson){
1896
2088
  const total = reportJson.summary && reportJson.summary.total_cost_usd;
1897
2089
  const sav = reportJson.summary && reportJson.summary.estimated_savings_usd;
@@ -1904,11 +2096,16 @@ async function load(){
1904
2096
  document.getElementById('scanMeta').textContent = '(no report.json yet \u2014 run: aiopt scan)';
1905
2097
  }
1906
2098
 
1907
- const usageTxt = await fetch('/api/usage.jsonl').then(r=>r.ok?r.text():null);
2099
+ const usageTxt = await fetch('/api/usage.jsonl', { cache: 'no-store' }).then(r=>r.ok?r.text():null).catch(()=>null);
1908
2100
  if(usageTxt){
1909
- // 7d cost trend: sum(cost_usd) per day from usage.jsonl (ev.ts).
1910
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).
1911
2107
  const bins = Array.from({length:7}, (_,i)=>({ day:i, cost:0, calls:0 }));
2108
+
1912
2109
  for(const line of usageTxt.trim().split('
1913
2110
  ')){
1914
2111
  if(!line) continue;
@@ -1916,52 +2113,108 @@ async function load(){
1916
2113
  const ev = JSON.parse(line);
1917
2114
  const t = Date.parse(ev.ts);
1918
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
1919
2128
  const d = Math.floor((now - t) / 86400000);
1920
2129
  if(d>=0 && d<7){
1921
2130
  bins[d].calls++;
1922
- const c = Number(ev.cost_usd);
1923
- if(Number.isFinite(c)) bins[d].cost += c;
2131
+ bins[d].cost += c;
1924
2132
  }
1925
2133
  }catch{}
1926
2134
  }
1927
2135
 
1928
- // SVG sparkline
1929
- const W=520, H=120, P=12;
1930
- const pts = bins.slice().reverse();
1931
- const max = Math.max(...pts.map(b=>b.cost), 0.000001);
1932
- const xs = pts.map((_,i)=> P + (i*(W-2*P))/6);
1933
- const ys = pts.map(b=> H-P - ((b.cost/max)*(H-2*P)) );
1934
- let d = '';
1935
- for(let i=0;i<xs.length;i++) d += (i===0?'M':'L') + xs[i].toFixed(1)+','+ys[i].toFixed(1)+' ';
1936
- const area = 'M'+xs[0].toFixed(1)+','+(H-P).toFixed(1)+' ' + d + 'L'+xs[xs.length-1].toFixed(1)+','+(H-P).toFixed(1)+' Z';
1937
-
1938
- 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('');
1939
- const svg =
1940
- '<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">'+
1941
- '<path d="'+area+'" fill="rgba(96,165,250,.12)" />'+
1942
- '<path d="'+d+'" fill="none" stroke="rgba(96,165,250,.95)" stroke-width="2" />'+
1943
- circles+
1944
- '<text x="'+P+'" y="'+(P+10)+'" fill="rgba(229,231,235,.75)" font-size="11">max '+money(max)+'</text>'+
1945
- '</svg>';
1946
- document.getElementById('trendSvg').innerHTML = svg;
1947
-
1948
- // Text fallback/detail
1949
- const rows = pts.map((b,idx)=>{
1950
- const label = (idx===pts.length-1 ? 'd-6' : (idx===0 ? 'today' : ('d-'+idx)));
1951
- const dollars = ('$' + (Math.round(b.cost*100)/100).toFixed(2));
1952
- return String(label).padEnd(7) + ' ' + String(dollars).padStart(9) + ' (' + b.calls + ' calls)';
1953
- });
1954
- document.getElementById('trend').textContent = rows.join('
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('
2161
+ ');
2162
+
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
+ }
2168
+ }
2169
+
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('
1955
2197
  ');
2198
+ }
2199
+
1956
2200
  } else {
2201
+ document.getElementById('liveText').textContent = '(no usage.jsonl yet)';
2202
+ document.getElementById('liveSvg').innerHTML = '';
1957
2203
  document.getElementById('trend').textContent = '(no usage.jsonl yet)';
1958
2204
  document.getElementById('trendSvg').innerHTML = '';
2205
+ const liveEl = document.getElementById('live');
2206
+ if(liveEl) liveEl.textContent = 'live: off';
1959
2207
  }
1960
2208
 
1961
- const reportMd = await fetch('/api/report.md').then(r=>r.ok?r.text():null);
2209
+ const reportMd = await fetch('/api/report.md').then(r=>r.ok?r.text():null).catch(()=>null);
1962
2210
  document.getElementById('scan').textContent = reportMd || '(no report.md yet \u2014 run: aiopt scan)';
1963
2211
  }
2212
+
2213
+ // Auto-refresh (simple polling): updates the dashboard as files change.
1964
2214
  load();
2215
+ setInterval(()=>{ load(); }, 2000);
2216
+ const liveEl = document.getElementById('live');
2217
+ if(liveEl) liveEl.textContent = 'live: on (polling)';
1965
2218
  </script>
1966
2219
  </body>
1967
2220
  </html>`;
@@ -1975,13 +2228,15 @@ load();
1975
2228
  if (url.startsWith("/api/")) {
1976
2229
  const name = url.replace("/api/", "");
1977
2230
  if (name === "_meta") {
2231
+ ensureUsageFile();
1978
2232
  const expected = ["guard-last.txt", "guard-last.json", "report.json", "report.md", "usage.jsonl", "guard-history.jsonl"];
1979
- const missing = expected.filter((f) => !import_fs10.default.existsSync(file(f)));
2233
+ const missing = expected.filter((f) => !import_fs11.default.existsSync(file(f)));
1980
2234
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1981
2235
  res.end(JSON.stringify({ baseDir: cwd, outDir, missing }, null, 2));
1982
2236
  return;
1983
2237
  }
1984
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();
1985
2240
  if (!allow.has(name)) {
1986
2241
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
1987
2242
  res.end("not found");
@@ -2011,13 +2266,13 @@ load();
2011
2266
  await new Promise(() => {
2012
2267
  });
2013
2268
  }
2014
- var import_http, import_fs10, import_path11;
2269
+ var import_http, import_fs11, import_path12;
2015
2270
  var init_dashboard = __esm({
2016
2271
  "src/dashboard.ts"() {
2017
2272
  "use strict";
2018
2273
  import_http = __toESM(require("http"));
2019
- import_fs10 = __toESM(require("fs"));
2020
- import_path11 = __toESM(require("path"));
2274
+ import_fs11 = __toESM(require("fs"));
2275
+ import_path12 = __toESM(require("path"));
2021
2276
  }
2022
2277
  });
2023
2278
 
@@ -2027,55 +2282,55 @@ __export(find_output_exports, {
2027
2282
  findAioptOutputDir: () => findAioptOutputDir
2028
2283
  });
2029
2284
  function findAioptOutputDir(startCwd) {
2030
- let cur = import_path12.default.resolve(startCwd);
2285
+ let cur = import_path13.default.resolve(startCwd);
2031
2286
  while (true) {
2032
- const outDir = import_path12.default.join(cur, "aiopt-output");
2033
- if (import_fs11.default.existsSync(outDir)) {
2287
+ const outDir = import_path13.default.join(cur, "aiopt-output");
2288
+ if (import_fs12.default.existsSync(outDir)) {
2034
2289
  try {
2035
- if (import_fs11.default.statSync(outDir).isDirectory()) return { cwd: cur, outDir };
2290
+ if (import_fs12.default.statSync(outDir).isDirectory()) return { cwd: cur, outDir };
2036
2291
  } catch {
2037
2292
  }
2038
2293
  }
2039
- const parent = import_path12.default.dirname(cur);
2294
+ const parent = import_path13.default.dirname(cur);
2040
2295
  if (parent === cur) break;
2041
2296
  cur = parent;
2042
2297
  }
2043
2298
  try {
2044
- const base = import_path12.default.resolve(startCwd);
2045
- const children = import_fs11.default.readdirSync(base, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => import_path12.default.join(base, d.name));
2299
+ const base = import_path13.default.resolve(startCwd);
2300
+ const children = import_fs12.default.readdirSync(base, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => import_path13.default.join(base, d.name));
2046
2301
  for (const child of children) {
2047
- const outDir = import_path12.default.join(child, "aiopt-output");
2048
- if (import_fs11.default.existsSync(outDir)) {
2302
+ const outDir = import_path13.default.join(child, "aiopt-output");
2303
+ if (import_fs12.default.existsSync(outDir)) {
2049
2304
  try {
2050
- if (import_fs11.default.statSync(outDir).isDirectory()) return { cwd: child, outDir };
2305
+ if (import_fs12.default.statSync(outDir).isDirectory()) return { cwd: child, outDir };
2051
2306
  } catch {
2052
2307
  }
2053
2308
  }
2054
2309
  }
2055
2310
  } catch {
2056
2311
  }
2057
- return { cwd: import_path12.default.resolve(startCwd), outDir: import_path12.default.join(import_path12.default.resolve(startCwd), "aiopt-output") };
2312
+ return { cwd: import_path13.default.resolve(startCwd), outDir: import_path13.default.join(import_path13.default.resolve(startCwd), "aiopt-output") };
2058
2313
  }
2059
- var import_fs11, import_path12;
2314
+ var import_fs12, import_path13;
2060
2315
  var init_find_output = __esm({
2061
2316
  "src/find-output.ts"() {
2062
2317
  "use strict";
2063
- import_fs11 = __toESM(require("fs"));
2064
- import_path12 = __toESM(require("path"));
2318
+ import_fs12 = __toESM(require("fs"));
2319
+ import_path13 = __toESM(require("path"));
2065
2320
  }
2066
2321
  });
2067
2322
 
2068
2323
  // src/rates-util.ts
2069
2324
  function loadRateTableFromDistPath() {
2070
- const p = import_path13.default.join(__dirname, "..", "rates", "rate_table.json");
2071
- return JSON.parse(import_fs12.default.readFileSync(p, "utf8"));
2325
+ const p = import_path14.default.join(__dirname, "..", "rates", "rate_table.json");
2326
+ return JSON.parse(import_fs13.default.readFileSync(p, "utf8"));
2072
2327
  }
2073
- var import_fs12, import_path13;
2328
+ var import_fs13, import_path14;
2074
2329
  var init_rates_util = __esm({
2075
2330
  "src/rates-util.ts"() {
2076
2331
  "use strict";
2077
- import_fs12 = __toESM(require("fs"));
2078
- import_path13 = __toESM(require("path"));
2332
+ import_fs13 = __toESM(require("fs"));
2333
+ import_path14 = __toESM(require("path"));
2079
2334
  }
2080
2335
  });
2081
2336
 
@@ -2086,8 +2341,8 @@ __export(quickstart_exports, {
2086
2341
  seedDemoUsage: () => seedDemoUsage
2087
2342
  });
2088
2343
  function seedDemoUsage(outDir) {
2089
- import_fs13.default.mkdirSync(outDir, { recursive: true });
2090
- const p = import_path14.default.join(outDir, "usage.jsonl");
2344
+ import_fs14.default.mkdirSync(outDir, { recursive: true });
2345
+ const p = import_path15.default.join(outDir, "usage.jsonl");
2091
2346
  const now = Date.now();
2092
2347
  const lines = [];
2093
2348
  for (let i = 0; i < 60; i++) {
@@ -2104,18 +2359,18 @@ function seedDemoUsage(outDir) {
2104
2359
  meta: { feature_tag: i % 2 ? "summarize" : "coding" }
2105
2360
  });
2106
2361
  }
2107
- import_fs13.default.writeFileSync(p, lines.map((x) => JSON.stringify(x)).join("\n") + "\n");
2362
+ import_fs14.default.writeFileSync(p, lines.map((x) => JSON.stringify(x)).join("\n") + "\n");
2108
2363
  return p;
2109
2364
  }
2110
2365
  function runQuickstart(cwd, opts) {
2111
- const outDir = import_path14.default.join(cwd, "aiopt-output");
2366
+ const outDir = import_path15.default.join(cwd, "aiopt-output");
2112
2367
  const usagePath = seedDemoUsage(outDir);
2113
2368
  const rt = loadRateTableFromDistPath();
2114
2369
  const { readJsonl: readJsonl2 } = (init_io(), __toCommonJS(io_exports));
2115
2370
  const events = readJsonl2(usagePath);
2116
2371
  const { analysis, savings, policy, meta } = analyze(rt, events);
2117
- import_fs13.default.writeFileSync(import_path14.default.join(outDir, "analysis.json"), JSON.stringify(analysis, null, 2));
2118
- import_fs13.default.writeFileSync(import_path14.default.join(outDir, "report.json"), JSON.stringify({
2372
+ import_fs14.default.writeFileSync(import_path15.default.join(outDir, "analysis.json"), JSON.stringify(analysis, null, 2));
2373
+ import_fs14.default.writeFileSync(import_path15.default.join(outDir, "report.json"), JSON.stringify({
2119
2374
  version: 3,
2120
2375
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
2121
2376
  confidence: analysis.unknown_models?.length ? "MEDIUM" : "HIGH",
@@ -2135,8 +2390,8 @@ function runQuickstart(cwd, opts) {
2135
2390
  unknown_models: analysis.unknown_models || [],
2136
2391
  notes: []
2137
2392
  }, null, 2));
2138
- import_fs13.default.writeFileSync(import_path14.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
2139
- import_fs13.default.writeFileSync(import_path14.default.join(outDir, "report.md"), "# AIOpt quickstart demo\n\nThis is a demo report generated by `aiopt quickstart --demo`.\n");
2393
+ import_fs14.default.writeFileSync(import_path15.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
2394
+ import_fs14.default.writeFileSync(import_path15.default.join(outDir, "report.md"), "# AIOpt quickstart demo\n\nThis is a demo report generated by `aiopt quickstart --demo`.\n");
2140
2395
  const r = runGuard(rt, {
2141
2396
  baselineEvents: events,
2142
2397
  candidate: {
@@ -2147,12 +2402,12 @@ function runQuickstart(cwd, opts) {
2147
2402
  });
2148
2403
  return { usagePath, outDir, guard: r, port: opts.port };
2149
2404
  }
2150
- var import_fs13, import_path14;
2405
+ var import_fs14, import_path15;
2151
2406
  var init_quickstart = __esm({
2152
2407
  "src/quickstart.ts"() {
2153
2408
  "use strict";
2154
- import_fs13 = __toESM(require("fs"));
2155
- import_path14 = __toESM(require("path"));
2409
+ import_fs14 = __toESM(require("fs"));
2410
+ import_path15 = __toESM(require("path"));
2156
2411
  init_scan();
2157
2412
  init_rates_util();
2158
2413
  init_guard();
@@ -2160,8 +2415,8 @@ var init_quickstart = __esm({
2160
2415
  });
2161
2416
 
2162
2417
  // src/cli.ts
2163
- var import_fs14 = __toESM(require("fs"));
2164
- var import_path15 = __toESM(require("path"));
2418
+ var import_fs15 = __toESM(require("fs"));
2419
+ var import_path16 = __toESM(require("path"));
2165
2420
  var import_commander = require("commander");
2166
2421
  init_io();
2167
2422
  init_scan();
@@ -2169,17 +2424,17 @@ var program = new import_commander.Command();
2169
2424
  var DEFAULT_INPUT = "./aiopt-output/usage.jsonl";
2170
2425
  var DEFAULT_OUTPUT_DIR = "./aiopt-output";
2171
2426
  function loadRateTable() {
2172
- const p = import_path15.default.join(__dirname, "..", "rates", "rate_table.json");
2173
- return JSON.parse(import_fs14.default.readFileSync(p, "utf8"));
2427
+ const p = import_path16.default.join(__dirname, "..", "rates", "rate_table.json");
2428
+ return JSON.parse(import_fs15.default.readFileSync(p, "utf8"));
2174
2429
  }
2175
2430
  program.name("aiopt").description("AI \uBE44\uC6A9 \uC790\uB3D9 \uC808\uAC10 \uC778\uD504\uB77C \u2014 \uC11C\uBC84 \uC5C6\uB294 \uB85C\uCEEC CLI MVP").version(require_package().version);
2176
2431
  program.command("init").description("aiopt-input/ \uBC0F \uC0D8\uD50C usage.jsonl, aiopt-output/ \uC0DD\uC131").action(() => {
2177
2432
  ensureDir("./aiopt-input");
2178
2433
  ensureDir("./aiopt-output");
2179
- const sampleSrc = import_path15.default.join(__dirname, "..", "samples", "sample_usage.jsonl");
2180
- const dst = import_path15.default.join("./aiopt-input", "usage.jsonl");
2181
- if (!import_fs14.default.existsSync(dst)) {
2182
- import_fs14.default.copyFileSync(sampleSrc, dst);
2434
+ const sampleSrc = import_path16.default.join(__dirname, "..", "samples", "sample_usage.jsonl");
2435
+ const dst = import_path16.default.join("./aiopt-input", "usage.jsonl");
2436
+ if (!import_fs15.default.existsSync(dst)) {
2437
+ import_fs15.default.copyFileSync(sampleSrc, dst);
2183
2438
  console.log("Created ./aiopt-input/usage.jsonl (sample)");
2184
2439
  } else {
2185
2440
  console.log("Exists ./aiopt-input/usage.jsonl (skip)");
@@ -2189,7 +2444,7 @@ program.command("init").description("aiopt-input/ \uBC0F \uC0D8\uD50C usage.json
2189
2444
  program.command("scan").description("\uC785\uB825 \uB85C\uADF8(JSONL/CSV)\uB97C \uBD84\uC11D\uD558\uACE0 report.md/report.json + patches\uAE4C\uC9C0 \uC0DD\uC131").option("--input <path>", "input file path (default: ./aiopt-output/usage.jsonl)", DEFAULT_INPUT).option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).action(async (opts) => {
2190
2445
  const inputPath = String(opts.input);
2191
2446
  const outDir = String(opts.out);
2192
- if (!import_fs14.default.existsSync(inputPath)) {
2447
+ if (!import_fs15.default.existsSync(inputPath)) {
2193
2448
  console.error(`Input not found: ${inputPath}`);
2194
2449
  process.exit(1);
2195
2450
  }
@@ -2205,7 +2460,7 @@ program.command("scan").description("\uC785\uB825 \uB85C\uADF8(JSONL/CSV)\uB97C
2205
2460
  const tag = f.status === "no-issue" ? "(no issue detected)" : `($${Math.round(f.impact_usd * 100) / 100})`;
2206
2461
  console.log(`${i + 1}) ${f.title} ${tag}`);
2207
2462
  });
2208
- console.log(`Report: ${import_path15.default.join(outDir, "report.md")}`);
2463
+ console.log(`Report: ${import_path16.default.join(outDir, "report.md")}`);
2209
2464
  });
2210
2465
  program.command("policy").description("\uB9C8\uC9C0\uB9C9 scan \uACB0\uACFC \uAE30\uBC18\uC73C\uB85C cost-policy.json\uB9CC \uC7AC\uC0DD\uC131 (MVP: scan\uACFC \uB3D9\uC77C \uB85C\uC9C1)").option("--input <path>", "input file path (default: ./aiopt-input/usage.jsonl)", DEFAULT_INPUT).option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).action((opts) => {
2211
2466
  const inputPath = String(opts.input);
@@ -2215,7 +2470,7 @@ program.command("policy").description("\uB9C8\uC9C0\uB9C9 scan \uACB0\uACFC \uAE
2215
2470
  const { policy } = analyze(rt, events);
2216
2471
  policy.generated_from.input = inputPath;
2217
2472
  ensureDir(outDir);
2218
- import_fs14.default.writeFileSync(import_path15.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
2473
+ import_fs15.default.writeFileSync(import_path16.default.join(outDir, "cost-policy.json"), JSON.stringify(policy, null, 2));
2219
2474
  console.log(`OK: ${outDir}/cost-policy.json`);
2220
2475
  });
2221
2476
  program.command("install").description("Install AIOpt guardrails: create aiopt/ + policies + usage.jsonl").option("--force", "overwrite existing files").option("--seed-sample", "seed 1 sample line into aiopt-output/usage.jsonl").action(async (opts) => {
@@ -2255,7 +2510,7 @@ licenseCmd.command("verify").option("--path <path>", "license.json path (default
2255
2510
  const { DEFAULT_PUBLIC_KEY_PEM: DEFAULT_PUBLIC_KEY_PEM2, defaultLicensePath: defaultLicensePath2, readLicenseFile: readLicenseFile2, verifyLicenseKey: verifyLicenseKey2 } = await Promise.resolve().then(() => (init_license(), license_exports));
2256
2511
  const p = opts.path ? String(opts.path) : defaultLicensePath2(process.cwd());
2257
2512
  const pub = process.env.AIOPT_LICENSE_PUBKEY || DEFAULT_PUBLIC_KEY_PEM2;
2258
- if (!import_fs14.default.existsSync(p)) {
2513
+ if (!import_fs15.default.existsSync(p)) {
2259
2514
  console.error(`FAIL: license file not found: ${p}`);
2260
2515
  process.exit(3);
2261
2516
  }
@@ -2272,7 +2527,7 @@ licenseCmd.command("status").option("--path <path>", "license.json path (default
2272
2527
  const { DEFAULT_PUBLIC_KEY_PEM: DEFAULT_PUBLIC_KEY_PEM2, defaultLicensePath: defaultLicensePath2, readLicenseFile: readLicenseFile2, verifyLicenseKey: verifyLicenseKey2 } = await Promise.resolve().then(() => (init_license(), license_exports));
2273
2528
  const p = opts.path ? String(opts.path) : defaultLicensePath2(process.cwd());
2274
2529
  const pub = process.env.AIOPT_LICENSE_PUBKEY || DEFAULT_PUBLIC_KEY_PEM2;
2275
- if (!import_fs14.default.existsSync(p)) {
2530
+ if (!import_fs15.default.existsSync(p)) {
2276
2531
  console.log("NO_LICENSE");
2277
2532
  process.exit(2);
2278
2533
  }
@@ -2288,7 +2543,7 @@ licenseCmd.command("status").option("--path <path>", "license.json path (default
2288
2543
  program.command("gate").description("Merge gate (CI-friendly): fail (exit 1) when policy violations are detected; prints <=10 lines").option("--input <path>", "input usage jsonl/csv (default: ./aiopt-output/usage.jsonl)", DEFAULT_INPUT).option("--out <dir>", "output dir (default: ./aiopt-output)", DEFAULT_OUTPUT_DIR).action(async (opts) => {
2289
2544
  const inputPath = String(opts.input);
2290
2545
  const outDir = String(opts.out);
2291
- if (!import_fs14.default.existsSync(inputPath)) {
2546
+ if (!import_fs15.default.existsSync(inputPath)) {
2292
2547
  console.error(`FAIL: input not found: ${inputPath}`);
2293
2548
  process.exit(1);
2294
2549
  }
@@ -2332,11 +2587,11 @@ program.command("guard").description("Pre-deploy guardrail: compare baseline usa
2332
2587
  console.error("FAIL: diff mode requires both --baseline and --candidate");
2333
2588
  process.exit(3);
2334
2589
  }
2335
- if (!import_fs14.default.existsSync(baselinePath)) {
2590
+ if (!import_fs15.default.existsSync(baselinePath)) {
2336
2591
  console.error(`FAIL: baseline not found: ${baselinePath}`);
2337
2592
  process.exit(3);
2338
2593
  }
2339
- if (candidatePath && !import_fs14.default.existsSync(candidatePath)) {
2594
+ if (candidatePath && !import_fs15.default.existsSync(candidatePath)) {
2340
2595
  console.error(`FAIL: candidate not found: ${candidatePath}`);
2341
2596
  process.exit(3);
2342
2597
  }
@@ -2358,13 +2613,13 @@ program.command("guard").description("Pre-deploy guardrail: compare baseline usa
2358
2613
  });
2359
2614
  console.log(r.message);
2360
2615
  try {
2361
- const outDir = import_path15.default.resolve(DEFAULT_OUTPUT_DIR);
2362
- import_fs14.default.mkdirSync(outDir, { recursive: true });
2616
+ const outDir = import_path16.default.resolve(DEFAULT_OUTPUT_DIR);
2617
+ import_fs15.default.mkdirSync(outDir, { recursive: true });
2363
2618
  const ts = (/* @__PURE__ */ new Date()).toISOString();
2364
- import_fs14.default.writeFileSync(import_path15.default.join(outDir, "guard-last.txt"), r.message);
2365
- import_fs14.default.writeFileSync(import_path15.default.join(outDir, "guard-last.json"), JSON.stringify({ ts, exitCode: r.exitCode }, null, 2));
2619
+ import_fs15.default.writeFileSync(import_path16.default.join(outDir, "guard-last.txt"), r.message);
2620
+ import_fs15.default.writeFileSync(import_path16.default.join(outDir, "guard-last.json"), JSON.stringify({ ts, exitCode: r.exitCode }, null, 2));
2366
2621
  const histLine = JSON.stringify({ ts, exitCode: r.exitCode, mode: candidateEvents ? "diff" : "transform", baseline: baselinePath, candidate: candidatePath }) + "\n";
2367
- import_fs14.default.appendFileSync(import_path15.default.join(outDir, "guard-history.jsonl"), histLine);
2622
+ import_fs15.default.appendFileSync(import_path16.default.join(outDir, "guard-history.jsonl"), histLine);
2368
2623
  } catch {
2369
2624
  }
2370
2625
  process.exit(r.exitCode);
@@ -2392,14 +2647,14 @@ program.command("quickstart").description("1-minute demo: generate sample usage,
2392
2647
  console.log("--- guard ---");
2393
2648
  console.log(r.guard.message);
2394
2649
  try {
2395
- const fs15 = await import("fs");
2396
- const path16 = await import("path");
2397
- fs15.mkdirSync(r.outDir, { recursive: true });
2650
+ const fs16 = await import("fs");
2651
+ const path17 = await import("path");
2652
+ fs16.mkdirSync(r.outDir, { recursive: true });
2398
2653
  const ts = (/* @__PURE__ */ new Date()).toISOString();
2399
- fs15.writeFileSync(path16.join(r.outDir, "guard-last.txt"), r.guard.message);
2400
- fs15.writeFileSync(path16.join(r.outDir, "guard-last.json"), JSON.stringify({ ts, exitCode: r.guard.exitCode }, null, 2));
2654
+ fs16.writeFileSync(path17.join(r.outDir, "guard-last.txt"), r.guard.message);
2655
+ fs16.writeFileSync(path17.join(r.outDir, "guard-last.json"), JSON.stringify({ ts, exitCode: r.guard.exitCode }, null, 2));
2401
2656
  const histLine = JSON.stringify({ ts, exitCode: r.guard.exitCode, mode: "quickstart", baseline: r.usagePath, candidate: null }) + "\n";
2402
- fs15.appendFileSync(path16.join(r.outDir, "guard-history.jsonl"), histLine);
2657
+ fs16.appendFileSync(path17.join(r.outDir, "guard-history.jsonl"), histLine);
2403
2658
  } catch {
2404
2659
  }
2405
2660
  console.log("--- next ---");