@node9/proxy 1.11.12 → 1.11.13

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.
Files changed (4) hide show
  1. package/README.md +142 -244
  2. package/dist/cli.js +488 -391
  3. package/dist/cli.mjs +488 -391
  4. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -5712,6 +5712,12 @@ var init_ui = __esm({
5712
5712
  text-align: center;
5713
5713
  }
5714
5714
  /* results */
5715
+ .scan-date-range {
5716
+ text-align: center;
5717
+ font-size: 11px;
5718
+ color: var(--muted);
5719
+ padding: 6px 0 10px;
5720
+ }
5715
5721
  .scan-summary-row {
5716
5722
  display: grid;
5717
5723
  grid-template-columns: repeat(4, 1fr);
@@ -7604,13 +7610,10 @@ var init_ui = __esm({
7604
7610
  const res = await fetch('/scan', { headers: { 'X-Node9-Token': CSRF_TOKEN } });
7605
7611
  if (!res.ok) return;
7606
7612
  const data = await res.json();
7607
- if (data.status !== 'complete') return;
7613
+ if (data.status !== 'complete' || !data.summary) return;
7608
7614
  _scanData = data;
7609
- const sources = data.sources || [];
7610
- const total =
7611
- sources.reduce((n, s) => n + (s.findings || []).length, 0) +
7612
- sources.reduce((n, s) => n + (s.leaks || []).length, 0) +
7613
- sources.reduce((n, s) => n + (s.loops || []).length, 0);
7615
+ const v = data.summary.byVerdict || {};
7616
+ const total = (v.blocked || 0) + (v.supervised || 0) + (v.leaks || 0) + (v.loops || 0);
7614
7617
  if (total > 0) {
7615
7618
  const btn = document.getElementById('btn-scan-history');
7616
7619
  if (btn && !btn.querySelector('.scan-badge')) {
@@ -7715,42 +7718,26 @@ var init_ui = __esm({
7715
7718
  }
7716
7719
 
7717
7720
  function renderScanResults(data) {
7718
- if (!data || data.status !== 'complete') {
7721
+ if (!data || data.status !== 'complete' || !data.summary) {
7719
7722
  return '<div style="text-align:center;color:var(--muted);padding:32px">No data returned.</div>';
7720
7723
  }
7721
- const s = data.summary || {};
7722
- const sources = data.sources || [];
7723
-
7724
- // Flatten all findings across sources
7725
- const allFindings = sources.flatMap((src) => src.findings || []);
7726
- const allLeaks = sources.flatMap((src) => src.leaks || []);
7727
- const allLoops = sources.flatMap((src) => src.loops || []);
7728
-
7729
- // Split findings into three distinct buckets
7730
- const blocked = allFindings.filter((f) => f.verdict === 'block' && f.ruleSource !== 'user');
7731
- const supervised = allFindings.filter(
7732
- (f) => f.verdict !== 'block' && f.ruleSource !== 'user'
7733
- );
7734
- const yourRules = allFindings.filter((f) => f.ruleSource === 'user');
7735
-
7736
- // Loop cost estimate: each wasted iteration ~2K tokens at Sonnet pricing
7737
- const LOOP_THRESHOLD = 3;
7738
- const COST_PER_ITER = 0.006;
7739
- const wastedIters = allLoops.reduce(
7740
- (sum, l) => sum + Math.max(0, (l.count || 0) - LOOP_THRESHOLD),
7741
- 0
7742
- );
7743
- const loopWastedUSD = wastedIters * COST_PER_ITER;
7744
-
7745
- const totalCost = s.totalCostUSD || 0;
7724
+ // Server-computed ScanSummary (see src/scan-summary.ts).
7725
+ // This renderer is a pure presentation layer \u2014 no categorization here.
7726
+ const summary = data.summary;
7727
+ const stats = summary.stats || {};
7728
+ const byVerdict = summary.byVerdict || { blocked: 0, supervised: 0, leaks: 0, loops: 0 };
7729
+ const byAgent = summary.byAgent || [];
7730
+ const sections = summary.sections || [];
7731
+ const leaks = summary.leaks || [];
7732
+ const loops = summary.loops || [];
7733
+ const loopWastedUSD = summary.loopWastedUSD || 0;
7734
+ const totalCost = stats.totalCostUSD || 0;
7746
7735
 
7747
7736
  function fmtUSD(n) {
7748
7737
  if (!n || n < 0.001) return null;
7749
7738
  if (n < 1) return '$' + n.toFixed(3);
7750
7739
  return '$' + n.toFixed(2);
7751
7740
  }
7752
-
7753
- // \u2500\u2500 Hero stats row \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
7754
7741
  const costStr = fmtUSD(totalCost);
7755
7742
  const savingsStr = fmtUSD(loopWastedUSD);
7756
7743
 
@@ -7772,10 +7759,46 @@ var init_ui = __esm({
7772
7759
  );
7773
7760
  }
7774
7761
 
7775
- const stats =
7762
+ // \u2500\u2500 Date range header \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
7763
+ function fmtShort(iso) {
7764
+ if (!iso) return null;
7765
+ try {
7766
+ return new Date(iso).toLocaleDateString([], {
7767
+ month: 'short',
7768
+ day: 'numeric',
7769
+ year: 'numeric',
7770
+ });
7771
+ } catch {
7772
+ return null;
7773
+ }
7774
+ }
7775
+ const firstLabel = fmtShort(stats.firstDate);
7776
+ const lastLabel = fmtShort(stats.lastDate);
7777
+ let daySpan = '';
7778
+ if (stats.firstDate && stats.lastDate) {
7779
+ const days =
7780
+ Math.round(
7781
+ (new Date(stats.lastDate).getTime() - new Date(stats.firstDate).getTime()) /
7782
+ (1000 * 60 * 60 * 24)
7783
+ ) + 1;
7784
+ if (days > 1) daySpan = ' \xB7 ' + days + ' day' + (days !== 1 ? 's' : '');
7785
+ }
7786
+ const dateRangeHtml =
7787
+ firstLabel && lastLabel
7788
+ ? '<div class="scan-date-range">\u{1F4C5} ' +
7789
+ esc(firstLabel) +
7790
+ ' \u2013 ' +
7791
+ esc(lastLabel) +
7792
+ esc(daySpan) +
7793
+ '</div>'
7794
+ : '';
7795
+
7796
+ // \u2500\u2500 Hero stats row (by VERDICT \u2014 matches terminal) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7797
+ const statsHtml =
7798
+ dateRangeHtml +
7776
7799
  '<div class="scan-summary-row">' +
7777
7800
  '<div class="scan-stat">' +
7778
- statVal(s.sessions || 0, '') +
7801
+ statVal(stats.sessions || 0, '') +
7779
7802
  '<div class="scan-stat-label">Sessions</div></div>' +
7780
7803
  (totalCost > 0
7781
7804
  ? '<div class="scan-stat">' +
@@ -7783,19 +7806,16 @@ var init_ui = __esm({
7783
7806
  '<div class="scan-stat-label">AI Spend</div></div>'
7784
7807
  : '') +
7785
7808
  '<div class="scan-stat">' +
7786
- statVal(blocked.length, blocked.length ? 'danger' : 'ok') +
7809
+ statVal(byVerdict.blocked || 0, byVerdict.blocked ? 'danger' : 'ok') +
7787
7810
  '<div class="scan-stat-label">Blocked</div></div>' +
7788
7811
  '<div class="scan-stat">' +
7789
- statVal(supervised.length, supervised.length ? 'warning' : 'ok') +
7812
+ statVal(byVerdict.supervised || 0, byVerdict.supervised ? 'warning' : 'ok') +
7790
7813
  '<div class="scan-stat-label">Supervised</div></div>' +
7791
7814
  '<div class="scan-stat">' +
7792
- statVal(yourRules.length, '', yourRules.length ? 'color:#58a6ff' : '') +
7793
- '<div class="scan-stat-label">Your Rules</div></div>' +
7794
- '<div class="scan-stat">' +
7795
- statVal(allLeaks.length, allLeaks.length ? 'danger' : 'ok') +
7815
+ statVal(byVerdict.leaks || 0, byVerdict.leaks ? 'danger' : 'ok') +
7796
7816
  '<div class="scan-stat-label">Leaks</div></div>' +
7797
7817
  '<div class="scan-stat">' +
7798
- statVal(allLoops.length, allLoops.length ? 'warning' : 'ok') +
7818
+ statVal(byVerdict.loops || 0, byVerdict.loops ? 'warning' : 'ok') +
7799
7819
  '<div class="scan-stat-label">Loops</div></div>' +
7800
7820
  '</div>' +
7801
7821
  (savingsStr
@@ -7804,56 +7824,103 @@ var init_ui = __esm({
7804
7824
  '</span> in unnecessary LLM turns</div>'
7805
7825
  : '');
7806
7826
 
7807
- if (!allFindings.length && !allLeaks.length && !allLoops.length) {
7827
+ const hasNothing = !sections.length && !leaks.length && !loops.length;
7828
+ if (hasNothing) {
7808
7829
  return (
7809
- stats +
7830
+ statsHtml +
7810
7831
  '<div style="text-align:center;color:#57ab5a;padding:28px 0;font-size:13px;line-height:1.8">\u2705 <strong>No issues found</strong><br><span style="font-size:11px;color:var(--muted)">Scanned ' +
7811
- (s.sessions || 0) +
7812
- ' sessions across ' +
7813
- sources.length +
7814
- ' AI source(s)</span></div>'
7832
+ (stats.sessions || 0) +
7833
+ ' sessions</span></div>'
7815
7834
  );
7816
7835
  }
7817
7836
 
7818
- // \u2500\u2500 Helpers \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
7819
- function groupByKey(items, keyFn) {
7820
- const map = new Map();
7821
- for (const item of items) {
7822
- const k = keyFn(item);
7823
- if (!map.has(k)) map.set(k, []);
7824
- map.get(k).push(item);
7837
+ // \u2500\u2500 Per-agent breakdown (server-computed byAgent) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7838
+ const agentBreakdown = byAgent.length
7839
+ ? '<div class="scan-agent-group">' +
7840
+ byAgent
7841
+ .map(
7842
+ (a) =>
7843
+ '<div class="scan-agent-row">' +
7844
+ '<span class="scan-agent-name">' +
7845
+ esc(a.icon || '') +
7846
+ ' ' +
7847
+ esc(a.label) +
7848
+ '</span>' +
7849
+ '<span class="scan-agent-stat">' +
7850
+ a.sessions +
7851
+ ' session' +
7852
+ (a.sessions !== 1 ? 's' : '') +
7853
+ '</span>' +
7854
+ '<span class="scan-agent-sep">\xB7</span>' +
7855
+ '<span class="scan-agent-stat">' +
7856
+ a.findings +
7857
+ ' finding' +
7858
+ (a.findings !== 1 ? 's' : '') +
7859
+ '</span>' +
7860
+ '</div>'
7861
+ )
7862
+ .join('') +
7863
+ '</div>'
7864
+ : '';
7865
+
7866
+ // \u2500\u2500 Rule row renderer \u2014 shared between rule sections + leaks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7867
+ // Determines bar-max across all sections so relative counts stay honest.
7868
+ let maxBar = 1;
7869
+ for (const section of sections) {
7870
+ for (const rule of section.rules || []) {
7871
+ if (rule.findings.length > maxBar) maxBar = rule.findings.length;
7825
7872
  }
7826
- return [...map.entries()].sort((a, b) => b[1].length - a[1].length);
7827
7873
  }
7874
+ // Group leaks by patternName for display
7875
+ const leaksByPattern = (function () {
7876
+ const m = new Map();
7877
+ for (const l of leaks) {
7878
+ const k = l.patternName || 'DLP';
7879
+ if (!m.has(k)) m.set(k, []);
7880
+ m.get(k).push(l);
7881
+ }
7882
+ for (const [, group] of m) if (group.length > maxBar) maxBar = group.length;
7883
+ return [...m.entries()].sort((a, b) => b[1].length - a[1].length);
7884
+ })();
7828
7885
 
7829
- function findingRow(item, type) {
7830
- const date = new Date(item.timestamp).toLocaleDateString([], {
7886
+ function findingRow(timestamp, text) {
7887
+ const date = new Date(timestamp).toLocaleDateString([], {
7831
7888
  month: 'short',
7832
7889
  day: 'numeric',
7833
7890
  });
7834
- const cmdText =
7835
- type === 'leak' ? esc(item.sample || '') : esc(truncate(item.command || '', 120));
7836
7891
  return (
7837
7892
  '<div class="scan-finding-row"><span class="scan-finding-ts">' +
7838
7893
  date +
7839
7894
  '</span><span class="scan-finding-cmd">' +
7840
- cmdText +
7895
+ text +
7841
7896
  '</span></div>'
7842
7897
  );
7843
7898
  }
7844
7899
 
7845
- function ruleSection(items, type, badgeClass, badgeLabel, maxBar) {
7846
- const keyFn =
7847
- type === 'leak'
7848
- ? (i) => i.pattern || 'DLP'
7849
- : (i) => (i.rule || 'unknown').replace(/^shield:[^:]+:/, '');
7850
- const grouped = groupByKey(items, keyFn);
7851
- return grouped
7852
- .map(([rule, group]) => {
7853
- const count = group.length;
7900
+ function ruleCardsForSection(section) {
7901
+ return (section.rules || [])
7902
+ .map((rule) => {
7903
+ const count = rule.findings.length;
7854
7904
  const barPct = Math.round((count / maxBar) * 100);
7855
7905
  const detailId = 'detail-' + Math.random().toString(36).slice(2);
7856
- const rows = group.map((i) => findingRow(i, type)).join('');
7906
+ const badgeClass =
7907
+ section.sourceType === 'user'
7908
+ ? 'badge-user'
7909
+ : rule.verdict === 'block'
7910
+ ? 'badge-block'
7911
+ : 'badge-review';
7912
+ const badgeLabel =
7913
+ section.sourceType === 'user'
7914
+ ? rule.verdict === 'block'
7915
+ ? 'YOUR BLOCK'
7916
+ : 'YOUR RULE'
7917
+ : rule.verdict === 'block'
7918
+ ? 'BLOCK'
7919
+ : 'REVIEW';
7920
+ const barColor = section.sourceType === 'user' ? ';background:#388bfd33' : '';
7921
+ const rows = rule.findings
7922
+ .map((f) => findingRow(f.timestamp, esc(truncate(f.command || '', 120))))
7923
+ .join('');
7857
7924
  return (
7858
7925
  '<div class="scan-rule-row" onclick="var d=document.getElementById(\\'' +
7859
7926
  detailId +
@@ -7864,12 +7931,12 @@ var init_ui = __esm({
7864
7931
  badgeLabel +
7865
7932
  '</span>' +
7866
7933
  '<span class="scan-rule-name">' +
7867
- esc(rule) +
7934
+ esc(rule.name) +
7868
7935
  '</span>' +
7869
7936
  '<span class="scan-rule-bar-wrap"><span class="scan-rule-bar" style="width:' +
7870
7937
  barPct +
7871
7938
  '%' +
7872
- (badgeClass === 'badge-user' ? ';background:#388bfd33' : '') +
7939
+ barColor +
7873
7940
  '"></span></span>' +
7874
7941
  '<span class="scan-rule-count">' +
7875
7942
  count +
@@ -7885,98 +7952,95 @@ var init_ui = __esm({
7885
7952
  .join('');
7886
7953
  }
7887
7954
 
7888
- // \u2500\u2500 Sections \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
7889
- const maxBar = Math.max(
7890
- ...allFindings.map((f) => 1),
7891
- ...groupByKey(allFindings, (f) => (f.rule || '').replace(/^shield:[^:]+:/, '')).map(
7892
- ([, v]) => v.length
7893
- ),
7894
- ...groupByKey(allLeaks, (f) => f.pattern || 'DLP').map(([, v]) => v.length),
7895
- 1
7896
- );
7955
+ // \u2500\u2500 Section headings by sourceType / id \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
7956
+ function sectionHeading(section) {
7957
+ if (section.id === 'default') {
7958
+ return '\u{1F441} ' + esc(section.label) + ' \u2014 built-in, always on';
7959
+ }
7960
+ if (section.sourceType === 'shield') {
7961
+ return (
7962
+ '\u{1F6E1} ' +
7963
+ esc(section.label) +
7964
+ ' shield' +
7965
+ (section.subtitle ? ' \u2014 ' + esc(section.subtitle) : '')
7966
+ );
7967
+ }
7968
+ if (section.id === 'cloud') {
7969
+ return '\u2601 Cloud Policy \u2014 synced from node9';
7970
+ }
7971
+ return '\u2705 Your Rules \u2014 working as configured';
7972
+ }
7973
+ function sectionColor(section) {
7974
+ if (section.blockedCount > 0) return '#e5534b';
7975
+ if (section.sourceType === 'user') return '#58a6ff';
7976
+ return 'var(--muted)';
7977
+ }
7897
7978
 
7898
- // Per-agent breakdown \u2014 one row per active agent (only shows active ones)
7899
- const agentBreakdown = (function () {
7900
- const rows = sources
7901
- .filter(
7902
- (src) =>
7903
- (src.sessions || 0) > 0 ||
7904
- (src.findings || []).length > 0 ||
7905
- (src.leaks || []).length > 0 ||
7906
- (src.loops || []).length > 0
7907
- )
7908
- .map((src) => {
7909
- const f = (src.findings || []).length;
7910
- const l = (src.leaks || []).length;
7911
- const lp = (src.loops || []).length;
7912
- const findingsTotal = f + l + lp;
7979
+ let html = statsHtml + agentBreakdown;
7980
+
7981
+ // \u2500\u2500 Leaks (shown first: credential exposure is highest-severity) \u2500\u2500\u2500\u2500\u2500
7982
+ if (leaksByPattern.length) {
7983
+ html +=
7984
+ '<div class="scan-rule-section-label" style="color:#e5534b">\u{1F511} Credential Leaks \u2014 secrets found in tool calls</div>';
7985
+ html += leaksByPattern
7986
+ .map(([pattern, group]) => {
7987
+ const count = group.length;
7988
+ const barPct = Math.round((count / maxBar) * 100);
7989
+ const detailId = 'detail-' + Math.random().toString(36).slice(2);
7990
+ const rows = group
7991
+ .map((l) => findingRow(l.timestamp, esc(l.redactedSample || '')))
7992
+ .join('');
7913
7993
  return (
7914
- '<div class="scan-agent-row">' +
7915
- '<span class="scan-agent-name">' +
7916
- esc(src.icon || '') +
7917
- ' ' +
7918
- esc(src.label || src.id) +
7919
- '</span>' +
7920
- '<span class="scan-agent-stat">' +
7921
- (src.sessions || 0) +
7922
- ' session' +
7923
- ((src.sessions || 0) !== 1 ? 's' : '') +
7994
+ '<div class="scan-rule-row" onclick="var d=document.getElementById(\\'' +
7995
+ detailId +
7996
+ "');var a=this.querySelector('.scan-rule-arrow');d.style.display=d.style.display===''?'none':'';a.textContent=d.style.display===''?'\u25B2':'\u25BC';\\">" +
7997
+ '<span class="scan-verdict-badge badge-dlp">DLP</span>' +
7998
+ '<span class="scan-rule-name">' +
7999
+ esc(pattern) +
7924
8000
  '</span>' +
7925
- '<span class="scan-agent-sep">\xB7</span>' +
7926
- '<span class="scan-agent-stat">' +
7927
- findingsTotal +
7928
- ' finding' +
7929
- (findingsTotal !== 1 ? 's' : '') +
8001
+ '<span class="scan-rule-bar-wrap"><span class="scan-rule-bar" style="width:' +
8002
+ barPct +
8003
+ '%"></span></span>' +
8004
+ '<span class="scan-rule-count">' +
8005
+ count +
7930
8006
  '</span>' +
8007
+ '<span class="scan-rule-arrow">\u25BC</span>' +
8008
+ '</div><div id="' +
8009
+ detailId +
8010
+ '" style="display:none" class="scan-rule-detail">' +
8011
+ rows +
7931
8012
  '</div>'
7932
8013
  );
7933
- });
7934
- if (!rows.length) return '';
7935
- return '<div class="scan-agent-group">' + rows.join('') + '</div>';
7936
- })();
7937
-
7938
- let html = stats + agentBreakdown;
7939
-
7940
- if (allLeaks.length) {
7941
- html +=
7942
- '<div class="scan-rule-section-label" style="color:#e5534b">\u{1F511} Credential Leaks \u2014 secrets found in tool calls</div>';
7943
- html += ruleSection(allLeaks, 'leak', 'badge-dlp', 'DLP', maxBar);
7944
- }
7945
-
7946
- if (blocked.length) {
7947
- html +=
7948
- '<div class="scan-rule-section-label" style="color:#e5534b">\u{1F6D1} Blocked \u2014 Node9 would have stopped these</div>';
7949
- html += ruleSection(blocked, 'risk', 'badge-block', 'BLOCK', maxBar);
7950
- }
7951
-
7952
- if (supervised.length) {
7953
- html +=
7954
- '<div class="scan-rule-section-label">\u{1F441} Supervised \u2014 Node9 would have asked you first</div>';
7955
- html += ruleSection(supervised, 'risk', 'badge-review', 'REVIEW', maxBar);
8014
+ })
8015
+ .join('');
7956
8016
  }
7957
8017
 
7958
- if (yourRules.length) {
8018
+ // \u2500\u2500 Sections (pre-grouped by source on the server) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8019
+ for (const section of sections) {
7959
8020
  html +=
7960
- '<div class="scan-rule-section-label" style="color:#58a6ff">\u2705 Your Rules \u2014 working as configured</div>';
7961
- html += ruleSection(yourRules, 'risk', 'badge-user', 'YOUR RULE', maxBar);
8021
+ '<div class="scan-rule-section-label" style="color:' +
8022
+ sectionColor(section) +
8023
+ '">' +
8024
+ sectionHeading(section) +
8025
+ '</div>';
8026
+ html += ruleCardsForSection(section);
7962
8027
  }
7963
8028
 
7964
- // Loops: single collapsed summary card, expand to see all groups
7965
- if (allLoops.length) {
7966
- const totalReps = allLoops.reduce((sum, l) => sum + (l.count || 0), 0);
8029
+ // \u2500\u2500 Loops: single collapsed summary card, expand to see all groups \u2500\u2500\u2500
8030
+ if (loops.length) {
8031
+ const totalReps = loops.reduce((sum, l) => sum + (l.count || 0), 0);
7967
8032
  const loopsDetailId = 'loops-detail-' + Math.random().toString(36).slice(2);
7968
8033
  const savingNote = savingsStr
7969
8034
  ? ' \xB7 <span style="color:var(--warning)">~' + savingsStr + ' wasted</span>'
7970
8035
  : '';
7971
- const loopGroupRows = allLoops
8036
+ const maxLoopCount = Math.max(...loops.map((l) => l.count || 0), 1);
8037
+ const loopGroupRows = loops
7972
8038
  .map((item) => {
7973
8039
  const date = new Date(item.timestamp).toLocaleDateString([], {
7974
8040
  month: 'short',
7975
8041
  day: 'numeric',
7976
8042
  });
7977
- const barPct = Math.round(
7978
- (item.count / Math.max(...allLoops.map((l) => l.count), 1)) * 100
7979
- );
8043
+ const barPct = Math.round((item.count / maxLoopCount) * 100);
7980
8044
  const detailId = 'detail-' + Math.random().toString(36).slice(2);
7981
8045
  return (
7982
8046
  '<div class="scan-rule-row" onclick="event.stopPropagation();var d=document.getElementById(\\'' +
@@ -8014,9 +8078,9 @@ var init_ui = __esm({
8014
8078
  "');var a=this.querySelector('.scan-rule-arrow');d.style.display=d.style.display===''?'none':'';a.textContent=d.style.display===''?'\u25B2':'\u25BC';\\">" +
8015
8079
  '<span class="scan-verdict-badge badge-review">LOOP</span>' +
8016
8080
  '<span class="scan-rule-name">' +
8017
- allLoops.length +
8081
+ loops.length +
8018
8082
  ' pattern' +
8019
- (allLoops.length !== 1 ? 's' : '') +
8083
+ (loops.length !== 1 ? 's' : '') +
8020
8084
  ' \xB7 ' +
8021
8085
  totalReps +
8022
8086
  ' total repetitions' +
@@ -8090,10 +8154,20 @@ var init_ui = __esm({
8090
8154
  const data = await res.json();
8091
8155
  results.style.display = 'block';
8092
8156
 
8093
- if (data.status === 'complete') {
8094
- const sources = data.sources || [];
8095
- const allRisks = sources.flatMap((s) => s.findings || []);
8096
- const allLeaks = sources.flatMap((s) => s.leaks || []);
8157
+ if (data.status === 'complete' && data.summary) {
8158
+ const summary = data.summary;
8159
+ const byVerdict = summary.byVerdict || {};
8160
+ const sections = summary.sections || [];
8161
+ const leaks = summary.leaks || [];
8162
+ // Flatten rule findings across all sections so we can show a Top-N list
8163
+ const allRisks = [];
8164
+ for (const section of sections) {
8165
+ for (const rule of section.rules || []) {
8166
+ for (const f of rule.findings) {
8167
+ allRisks.push({ rule: rule.name, verdict: rule.verdict, ...f });
8168
+ }
8169
+ }
8170
+ }
8097
8171
 
8098
8172
  let risksHtml = '';
8099
8173
  if (allRisks.length > 0) {
@@ -8106,7 +8180,7 @@ var init_ui = __esm({
8106
8180
  (r) => \`
8107
8181
  <div class="finding-item">
8108
8182
  <span class="finding-ts">\${new Date(r.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
8109
- <span class="finding-rule">\${r.rule.replace(/^shield:[^:]+:/, '')}</span>
8183
+ <span class="finding-rule">\${esc(r.rule)}</span>
8110
8184
  <span class="finding-cmd"><span class="finding-badge \${r.verdict === 'block' ? 'badge-block' : ''}">\${r.verdict === 'block' ? '\u{1F6D1}' : '\u{1F441}\uFE0F'}</span>\${esc(truncate(r.command, 120))}</span>
8111
8185
  </div>
8112
8186
  \`
@@ -8117,18 +8191,18 @@ var init_ui = __esm({
8117
8191
  }
8118
8192
 
8119
8193
  let leaksHtml = '';
8120
- if (allLeaks.length > 0) {
8194
+ if (leaks.length > 0) {
8121
8195
  leaksHtml = \`
8122
8196
  <div class="findings-list" style="border-color: rgba(201, 60, 55, 0.3);">
8123
8197
  <div style="font-size: 10px; font-weight: 700; color: #f87171; margin-bottom: 8px; text-transform: uppercase;">Credential Leaks Identified</div>
8124
- \${allLeaks
8198
+ \${leaks
8125
8199
  .slice(0, 5)
8126
8200
  .map(
8127
8201
  (l) => \`
8128
8202
  <div class="finding-item">
8129
8203
  <span class="finding-ts">\${new Date(l.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
8130
- <span class="finding-rule">\${esc(l.pattern)}</span>
8131
- <span class="finding-cmd"><span class="finding-badge badge-leak">\u{1F511}</span>\${esc(l.sample)}</span>
8204
+ <span class="finding-rule">\${esc(l.patternName)}</span>
8205
+ <span class="finding-cmd"><span class="finding-badge badge-leak">\u{1F511}</span>\${esc(l.redactedSample)}</span>
8132
8206
  </div>
8133
8207
  \`
8134
8208
  )
@@ -8137,10 +8211,11 @@ var init_ui = __esm({
8137
8211
  \`;
8138
8212
  }
8139
8213
 
8214
+ const riskCount = (byVerdict.blocked || 0) + (byVerdict.supervised || 0);
8140
8215
  results.innerHTML = \`
8141
8216
  <div class="insight-hint" style="color: #57ab5a; border-color: rgba(87, 171, 90, 0.4); background: rgba(87, 171, 90, 0.08);">
8142
8217
  <strong>\u2705 History Audit Complete</strong><br>
8143
- Scanned \${data.summary.sessions} sessions. Found <strong>\${data.summary.findings} risky operations</strong> and <strong>\${data.summary.dlp} secret leaks</strong>.
8218
+ Scanned \${summary.stats?.sessions || 0} sessions. Found <strong>\${riskCount} risky operations</strong> and <strong>\${byVerdict.leaks || 0} secret leaks</strong>.
8144
8219
  Node9 is now actively protecting you from these types of events.
8145
8220
  </div>
8146
8221
  \${leaksHtml}
@@ -8225,6 +8300,195 @@ var init_daemon_starter = __esm({
8225
8300
  }
8226
8301
  });
8227
8302
 
8303
+ // src/scan-summary.ts
8304
+ function buildScanSummary(agents) {
8305
+ const stats = {
8306
+ sessions: 0,
8307
+ totalToolCalls: 0,
8308
+ bashCalls: 0,
8309
+ totalCostUSD: 0,
8310
+ firstDate: null,
8311
+ lastDate: null
8312
+ };
8313
+ for (const a of agents) {
8314
+ stats.sessions += a.scan.sessions;
8315
+ stats.totalToolCalls += a.scan.totalToolCalls;
8316
+ stats.bashCalls += a.scan.bashCalls;
8317
+ stats.totalCostUSD += a.scan.totalCostUSD;
8318
+ if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
8319
+ stats.firstDate = a.scan.firstDate;
8320
+ }
8321
+ if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
8322
+ stats.lastDate = a.scan.lastDate;
8323
+ }
8324
+ }
8325
+ const allFindings = agents.flatMap((a) => a.scan.findings);
8326
+ const allLeaks = agents.flatMap(
8327
+ (a) => a.scan.dlpFindings.map((f) => ({
8328
+ patternName: f.patternName,
8329
+ redactedSample: f.redactedSample,
8330
+ toolName: f.toolName,
8331
+ timestamp: f.timestamp,
8332
+ project: f.project,
8333
+ sessionId: f.sessionId,
8334
+ agent: f.agent
8335
+ }))
8336
+ );
8337
+ const allLoops = agents.flatMap(
8338
+ (a) => a.scan.loopFindings.map((f) => ({
8339
+ toolName: f.toolName,
8340
+ commandPreview: f.commandPreview,
8341
+ count: f.count,
8342
+ timestamp: f.timestamp,
8343
+ project: f.project,
8344
+ sessionId: f.sessionId,
8345
+ agent: f.agent
8346
+ }))
8347
+ );
8348
+ const byVerdict = {
8349
+ blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
8350
+ supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
8351
+ leaks: allLeaks.length,
8352
+ loops: allLoops.length
8353
+ };
8354
+ const byAgent = agents.map((a) => ({
8355
+ id: a.id,
8356
+ label: a.label,
8357
+ icon: a.icon,
8358
+ sessions: a.scan.sessions,
8359
+ findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
8360
+ costUSD: a.scan.totalCostUSD
8361
+ })).filter((s) => s.sessions > 0 || s.findings > 0);
8362
+ const sections = buildSections(allFindings);
8363
+ const wastedIters = allLoops.reduce(
8364
+ (sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE),
8365
+ 0
8366
+ );
8367
+ const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
8368
+ return {
8369
+ stats,
8370
+ byVerdict,
8371
+ byAgent,
8372
+ sections,
8373
+ leaks: allLeaks,
8374
+ loops: allLoops,
8375
+ loopWastedUSD
8376
+ };
8377
+ }
8378
+ function buildSections(findings) {
8379
+ const sectionMap = /* @__PURE__ */ new Map();
8380
+ function ensureSection(id, label, subtitle, sourceType, shieldKey) {
8381
+ let s = sectionMap.get(id);
8382
+ if (!s) {
8383
+ s = {
8384
+ id,
8385
+ label,
8386
+ subtitle,
8387
+ sourceType,
8388
+ shieldKey,
8389
+ blockedCount: 0,
8390
+ reviewCount: 0,
8391
+ rules: []
8392
+ };
8393
+ sectionMap.set(id, s);
8394
+ }
8395
+ return s;
8396
+ }
8397
+ const ruleMap = /* @__PURE__ */ new Map();
8398
+ for (const f of findings) {
8399
+ const src = f.source;
8400
+ const sourceType = src.sourceType;
8401
+ const shieldName = src.shieldName;
8402
+ const verdict = src.rule.verdict === "block" ? "block" : "review";
8403
+ let sectionId;
8404
+ let sectionLabel;
8405
+ let sectionSubtitle;
8406
+ let shieldKey;
8407
+ if (sourceType === "default") {
8408
+ sectionId = "default";
8409
+ sectionLabel = "Default Rules";
8410
+ sectionSubtitle = "built-in, always on";
8411
+ } else if (sourceType === "shield") {
8412
+ sectionId = `shield:${shieldName}`;
8413
+ sectionLabel = shieldName;
8414
+ sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
8415
+ shieldKey = shieldName;
8416
+ } else if (shieldName === "cloud") {
8417
+ sectionId = "cloud";
8418
+ sectionLabel = "Cloud Policy";
8419
+ sectionSubtitle = "synced from node9 cloud";
8420
+ } else {
8421
+ sectionId = "user";
8422
+ sectionLabel = "Your Rules";
8423
+ sectionSubtitle = "added in node9.config.json";
8424
+ }
8425
+ const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
8426
+ const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
8427
+ const ruleKey = sectionId + "::" + ruleDisplayName;
8428
+ let rule = ruleMap.get(ruleKey);
8429
+ if (!rule) {
8430
+ rule = {
8431
+ name: ruleDisplayName,
8432
+ verdict,
8433
+ reason: src.rule.reason ?? "",
8434
+ findings: []
8435
+ };
8436
+ ruleMap.set(ruleKey, rule);
8437
+ section.rules.push(rule);
8438
+ }
8439
+ const cmdPreview = previewCommand(f.input, 120);
8440
+ const fullCmd = fullCommandOf(f.input);
8441
+ const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
8442
+ if (!isDupe) {
8443
+ rule.findings.push({
8444
+ timestamp: f.timestamp ?? "",
8445
+ command: cmdPreview,
8446
+ fullCommand: fullCmd,
8447
+ project: f.project,
8448
+ sessionId: f.sessionId,
8449
+ agent: f.agent,
8450
+ toolName: f.toolName
8451
+ });
8452
+ }
8453
+ if (verdict === "block") section.blockedCount++;
8454
+ else section.reviewCount++;
8455
+ }
8456
+ const sections = [...sectionMap.values()];
8457
+ sections.sort((a, b) => {
8458
+ const aTotal = a.blockedCount + a.reviewCount;
8459
+ const bTotal = b.blockedCount + b.reviewCount;
8460
+ if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
8461
+ return bTotal - aTotal;
8462
+ });
8463
+ for (const s of sections) {
8464
+ s.rules.sort((a, b) => {
8465
+ const aBlock = a.verdict === "block" ? 1 : 0;
8466
+ const bBlock = b.verdict === "block" ? 1 : 0;
8467
+ if (bBlock !== aBlock) return bBlock - aBlock;
8468
+ return b.findings.length - a.findings.length;
8469
+ });
8470
+ }
8471
+ return sections;
8472
+ }
8473
+ function previewCommand(input, max) {
8474
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
8475
+ const s = String(raw).replace(/\s+/g, " ").trim();
8476
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
8477
+ }
8478
+ function fullCommandOf(input) {
8479
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
8480
+ return String(raw).replace(/\s+/g, " ").trim();
8481
+ }
8482
+ var LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD;
8483
+ var init_scan_summary = __esm({
8484
+ "src/scan-summary.ts"() {
8485
+ "use strict";
8486
+ init_shields();
8487
+ LOOP_THRESHOLD_FOR_WASTE = 3;
8488
+ COST_PER_LOOP_ITER_USD = 6e-3;
8489
+ }
8490
+ });
8491
+
8228
8492
  // src/cli/commands/scan.ts
8229
8493
  import chalk2 from "chalk";
8230
8494
  import fs13 from "fs";
@@ -8269,10 +8533,6 @@ function preview(input, max) {
8269
8533
  const s = String(cmd).replace(/\s+/g, " ").trim();
8270
8534
  return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
8271
8535
  }
8272
- function fullCommand(input) {
8273
- const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
8274
- return String(cmd).replace(/\s+/g, " ").trim();
8275
- }
8276
8536
  function detectLoops(calls, project, sessionId, agent) {
8277
8537
  const counts = /* @__PURE__ */ new Map();
8278
8538
  for (const call of calls) {
@@ -9043,26 +9303,32 @@ function printFindingRow(f, drillDown, showSessionId, previewWidth) {
9043
9303
  const ts = f.timestamp ? chalk2.dim(fmtTs(f.timestamp) + " ") : "";
9044
9304
  const proj = chalk2.dim(f.project.slice(0, 22).padEnd(22) + " ");
9045
9305
  const agentBadge = f.agent === "gemini" ? chalk2.blue("[Gemini] ") : f.agent === "codex" ? chalk2.magenta("[Codex] ") : chalk2.cyan("[Claude] ");
9046
- const cmd = drillDown ? chalk2.gray(fullCommand(f.input)) : chalk2.gray(preview(f.input, previewWidth));
9306
+ let cmdText;
9307
+ if (drillDown) {
9308
+ cmdText = f.fullCommand;
9309
+ } else {
9310
+ cmdText = f.command;
9311
+ if (cmdText.length > previewWidth) cmdText = cmdText.slice(0, previewWidth - 1) + "\u2026";
9312
+ }
9313
+ const cmd = chalk2.gray(cmdText);
9047
9314
  const sessionSuffix = showSessionId && f.sessionId ? chalk2.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
9048
9315
  console.log(` ${ts}${proj}${agentBadge}${cmd}${sessionSuffix}`);
9049
9316
  }
9050
- function printRuleGroup(ruleFindings, topN, drillDown, previewWidth) {
9051
- const rule = ruleFindings[0].source.rule;
9052
- const ruleCount = ruleFindings.length;
9317
+ function printRuleGroup(rule, topN, drillDown, previewWidth) {
9318
+ const findings = rule.findings;
9319
+ const ruleCount = findings.length;
9053
9320
  const countBadge = ruleCount > 1 ? chalk2.white(` \xD7${ruleCount}`) : "";
9054
- const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
9055
- const icon = verdictIcon(rule.verdict ?? "review");
9321
+ const icon = verdictIcon(rule.verdict);
9056
9322
  console.log(
9057
- " " + icon + " " + chalk2.white(shortName) + countBadge + (rule.reason ? chalk2.dim(` \u2014 ${rule.reason}`) : "")
9323
+ " " + icon + " " + chalk2.white(rule.name) + countBadge + (rule.reason ? chalk2.dim(` \u2014 ${rule.reason}`) : "")
9058
9324
  );
9059
- const shown = drillDown ? ruleFindings : ruleFindings.slice(0, topN);
9325
+ const shown = drillDown ? findings : findings.slice(0, topN);
9060
9326
  for (const f of shown) {
9061
9327
  printFindingRow(f, drillDown, drillDown, previewWidth);
9062
9328
  }
9063
- if (!drillDown && ruleFindings.length > topN) {
9329
+ if (!drillDown && findings.length > topN) {
9064
9330
  console.log(
9065
- chalk2.dim(` \u2026 and ${ruleFindings.length - topN} more (--drill-down for full list)`)
9331
+ chalk2.dim(` \u2026 and ${findings.length - topN} more (--drill-down for full list)`)
9066
9332
  );
9067
9333
  }
9068
9334
  }
@@ -9109,6 +9375,11 @@ function registerScanCommand(program2) {
9109
9375
  (done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done)
9110
9376
  );
9111
9377
  const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
9378
+ const summary = buildScanSummary([
9379
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
9380
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
9381
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
9382
+ ]);
9112
9383
  process.stdout.write("\r" + " ".repeat(60) + "\r");
9113
9384
  if (scan.filesScanned === 0) {
9114
9385
  console.log(chalk2.yellow(" No session history found."));
@@ -9171,76 +9442,27 @@ function registerScanCommand(program2) {
9171
9442
  );
9172
9443
  }
9173
9444
  console.log("");
9174
- const sections = [];
9175
- const defaultFindings = scan.findings.filter((f) => f.source.sourceType === "default");
9176
- if (defaultFindings.length > 0) {
9177
- sections.push({
9178
- label: "Default Rules",
9179
- subtitle: "built-in, always on",
9180
- findings: defaultFindings
9181
- });
9182
- }
9183
- const byShield = /* @__PURE__ */ new Map();
9184
- for (const f of scan.findings.filter((f2) => f2.source.sourceType === "shield")) {
9185
- const arr = byShield.get(f.source.shieldName) ?? [];
9186
- arr.push(f);
9187
- byShield.set(f.source.shieldName, arr);
9188
- }
9189
- const shieldsWithFindings = [...byShield.entries()].sort(
9190
- (a, b) => b[1].length - a[1].length
9191
- );
9192
- for (const [shieldName, findings] of shieldsWithFindings) {
9193
- const description = SHIELDS[shieldName]?.description ?? "";
9194
- sections.push({
9195
- label: shieldName,
9196
- subtitle: description,
9197
- shieldKey: shieldName,
9198
- findings
9199
- });
9200
- }
9201
- const userFindings = scan.findings.filter(
9202
- (f) => f.source.sourceType === "user" || f.source.shieldName === "cloud"
9203
- );
9204
- if (userFindings.length > 0) {
9205
- sections.push({
9206
- label: "Your Rules",
9207
- subtitle: "added in node9.config.json",
9208
- findings: userFindings
9209
- });
9210
- }
9211
- for (const section of sections) {
9212
- const sectionBlocked = section.findings.filter(
9213
- (f) => f.source.rule.verdict === "block"
9214
- ).length;
9215
- const sectionReview = section.findings.length - sectionBlocked;
9445
+ for (const section of summary.sections) {
9216
9446
  const countParts = [];
9217
- if (sectionBlocked > 0) countParts.push(chalk2.red(`${sectionBlocked} blocked`));
9218
- if (sectionReview > 0) countParts.push(chalk2.yellow(`${sectionReview} review`));
9447
+ if (section.blockedCount > 0)
9448
+ countParts.push(chalk2.red(`${section.blockedCount} blocked`));
9449
+ if (section.reviewCount > 0)
9450
+ countParts.push(chalk2.yellow(`${section.reviewCount} review`));
9219
9451
  const countStr = countParts.join(chalk2.dim(" \xB7 "));
9220
9452
  const enableHint = section.shieldKey ? chalk2.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
9221
9453
  console.log(" " + chalk2.dim("\u2500".repeat(70)));
9222
9454
  console.log(
9223
9455
  " " + chalk2.bold(section.label) + (section.subtitle ? chalk2.dim(` \xB7 ${section.subtitle}`) : "") + " " + countStr + enableHint
9224
9456
  );
9225
- const byRule = /* @__PURE__ */ new Map();
9226
- for (const f of section.findings) {
9227
- const ruleKey = f.source.rule.name ?? "unnamed";
9228
- const arr = byRule.get(ruleKey) ?? [];
9229
- arr.push(f);
9230
- byRule.set(ruleKey, arr);
9231
- }
9232
- const sortedRules = [...byRule.entries()].sort((a, b) => {
9233
- const aBlock = a[1][0].source.rule.verdict === "block" ? 1 : 0;
9234
- const bBlock = b[1][0].source.rule.verdict === "block" ? 1 : 0;
9235
- if (bBlock !== aBlock) return bBlock - aBlock;
9236
- return b[1].length - a[1].length;
9237
- });
9238
- for (const [, ruleFindings] of sortedRules) {
9239
- printRuleGroup(ruleFindings, topN, drillDown, previewWidth);
9457
+ for (const rule of section.rules) {
9458
+ printRuleGroup(rule, topN, drillDown, previewWidth);
9240
9459
  }
9241
9460
  console.log("");
9242
9461
  }
9243
- const emptyShields = Object.keys(SHIELDS).filter((n) => !byShield.has(n)).sort();
9462
+ const activeShieldIds = new Set(
9463
+ summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
9464
+ );
9465
+ const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
9244
9466
  if (emptyShields.length > 0) {
9245
9467
  console.log(" " + chalk2.dim("\u2500".repeat(70)));
9246
9468
  console.log(
@@ -9350,78 +9572,12 @@ function registerScanCommand(program2) {
9350
9572
  const internalToken = getInternalToken();
9351
9573
  if (internalToken) {
9352
9574
  try {
9353
- const mapFindings = (arr, src) => (
9354
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9355
- arr.map((f) => ({
9356
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
9357
- rule: f.source?.rule?.name ?? f.source?.shieldLabel ?? "unnamed",
9358
- command: f.input?.command ?? f.input?.cmd ?? f.input?.file_path ?? f.toolName ?? "unknown",
9359
- verdict: f.source?.rule?.verdict ?? f.source?.rule?.action ?? "review",
9360
- ruleSource: f.source?.sourceType ?? "default",
9361
- source: src
9362
- }))
9363
- );
9364
- const mapLeaks = (arr, src) => (
9365
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9366
- arr.map((f) => ({
9367
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
9368
- pattern: f.patternName || "DLP",
9369
- sample: f.redactedSample || "********",
9370
- source: src
9371
- }))
9372
- );
9373
- const mapLoops = (arr, src) => (
9374
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9375
- arr.map((f) => ({
9376
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
9377
- toolName: f.toolName || "unknown",
9378
- commandPreview: f.commandPreview || "",
9379
- count: f.count || 0,
9380
- source: src
9381
- }))
9382
- );
9383
- const sources = [
9384
- {
9385
- id: "claude",
9386
- label: "Claude",
9387
- icon: "\u{1F916}",
9388
- sessions: claudeScan.sessions,
9389
- findings: mapFindings(claudeScan.findings, "claude"),
9390
- leaks: mapLeaks(claudeScan.dlpFindings, "claude"),
9391
- loops: mapLoops(claudeScan.loopFindings, "claude")
9392
- },
9393
- {
9394
- id: "gemini",
9395
- label: "Gemini",
9396
- icon: "\u264A",
9397
- sessions: geminiScan.sessions,
9398
- findings: mapFindings(geminiScan.findings, "gemini"),
9399
- leaks: mapLeaks(geminiScan.dlpFindings, "gemini"),
9400
- loops: mapLoops(geminiScan.loopFindings, "gemini")
9401
- },
9402
- {
9403
- id: "codex",
9404
- label: "Codex",
9405
- icon: "\u{1F52E}",
9406
- sessions: codexScan.sessions,
9407
- findings: mapFindings(codexScan.findings, "codex"),
9408
- leaks: mapLeaks(codexScan.dlpFindings, "codex"),
9409
- loops: mapLoops(codexScan.loopFindings, "codex")
9410
- }
9411
- ].filter(
9412
- (s) => s.sessions > 0 || s.findings.length > 0 || s.leaks.length > 0 || s.loops.length > 0
9413
- );
9414
- const payload = {
9415
- status: "complete",
9416
- summary: {
9417
- sessions: scan.sessions,
9418
- findings: scan.findings.length,
9419
- dlp: scan.dlpFindings.length,
9420
- loops: scan.loopFindings.length,
9421
- totalCostUSD: scan.totalCostUSD
9422
- },
9423
- sources
9424
- };
9575
+ const summary2 = buildScanSummary([
9576
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
9577
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
9578
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
9579
+ ]);
9580
+ const payload = { status: "complete", summary: summary2 };
9425
9581
  await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
9426
9582
  method: "POST",
9427
9583
  headers: { "Content-Type": "application/json", "x-node9-internal": internalToken },
@@ -9448,6 +9604,7 @@ var init_scan = __esm({
9448
9604
  init_dlp();
9449
9605
  init_daemon();
9450
9606
  init_daemon_starter();
9607
+ init_scan_summary();
9451
9608
  CLAUDE_PRICING = {
9452
9609
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
9453
9610
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -11430,9 +11587,21 @@ data: ${JSON.stringify(item.data)}
11430
11587
  const d = /* @__PURE__ */ new Date();
11431
11588
  d.setDate(d.getDate() - 90);
11432
11589
  d.setHours(0, 0, 0, 0);
11433
- let claude = { sessions: 0, findings: [], dlpFindings: [], loopFindings: [] };
11434
- let gemini = { sessions: 0, findings: [], dlpFindings: [], loopFindings: [] };
11435
- let codex = { sessions: 0, findings: [], dlpFindings: [], loopFindings: [] };
11590
+ const EMPTY_SCAN = {
11591
+ filesScanned: 0,
11592
+ sessions: 0,
11593
+ totalToolCalls: 0,
11594
+ bashCalls: 0,
11595
+ findings: [],
11596
+ dlpFindings: [],
11597
+ loopFindings: [],
11598
+ totalCostUSD: 0,
11599
+ firstDate: null,
11600
+ lastDate: null
11601
+ };
11602
+ let claude = EMPTY_SCAN;
11603
+ let gemini = EMPTY_SCAN;
11604
+ let codex = EMPTY_SCAN;
11436
11605
  try {
11437
11606
  claude = scanClaudeHistory(d);
11438
11607
  } catch (e) {
@@ -11448,86 +11617,13 @@ data: ${JSON.stringify(item.data)}
11448
11617
  } catch (e) {
11449
11618
  console.error("Codex scan failed:", e);
11450
11619
  }
11451
- const mapFindings = (arr, src) => (
11452
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11453
- arr.map((f) => ({
11454
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
11455
- rule: f.source?.rule?.name ?? f.source?.shieldLabel ?? "unnamed",
11456
- command: f.input?.command ?? f.input?.cmd ?? f.input?.file_path ?? f.toolName ?? "unknown",
11457
- verdict: f.source?.rule?.verdict ?? f.source?.rule?.action ?? "review",
11458
- ruleSource: f.source?.sourceType ?? "default",
11459
- source: src
11460
- }))
11461
- );
11462
- const mapLeaks = (arr, src) => (
11463
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11464
- arr.map((f) => ({
11465
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
11466
- pattern: f.patternName || "DLP",
11467
- sample: f.redactedSample || "********",
11468
- source: src
11469
- }))
11470
- );
11471
- const mapLoops = (arr, src) => (
11472
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11473
- arr.map((f) => ({
11474
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
11475
- toolName: f.toolName || "unknown",
11476
- commandPreview: f.commandPreview || "",
11477
- count: f.count || 0,
11478
- source: src
11479
- }))
11480
- );
11481
- const sources = [
11482
- {
11483
- id: "claude",
11484
- label: "Claude",
11485
- icon: "\u{1F916}",
11486
- sessions: claude.sessions,
11487
- findings: mapFindings(claude.findings, "claude"),
11488
- leaks: mapLeaks(claude.dlpFindings, "claude"),
11489
- loops: mapLoops(claude.loopFindings, "claude")
11490
- },
11491
- {
11492
- id: "gemini",
11493
- label: "Gemini",
11494
- icon: "\u264A",
11495
- sessions: gemini.sessions,
11496
- findings: mapFindings(gemini.findings, "gemini"),
11497
- leaks: mapLeaks(gemini.dlpFindings, "gemini"),
11498
- loops: mapLoops(gemini.loopFindings, "gemini")
11499
- },
11500
- {
11501
- id: "codex",
11502
- label: "Codex",
11503
- icon: "\u{1F52E}",
11504
- sessions: codex.sessions,
11505
- findings: mapFindings(codex.findings, "codex"),
11506
- leaks: mapLeaks(codex.dlpFindings, "codex"),
11507
- loops: mapLoops(codex.loopFindings, "codex")
11508
- }
11509
- ].filter(
11510
- (s) => s.sessions > 0 || s.findings.length > 0 || s.leaks.length > 0 || s.loops.length > 0
11511
- );
11512
- const totalSessions = claude.sessions + gemini.sessions + codex.sessions;
11513
- const totalFindings = claude.findings.length + gemini.findings.length + codex.findings.length;
11514
- const totalDlp = claude.dlpFindings.length + gemini.dlpFindings.length + codex.dlpFindings.length;
11515
- const totalLoops = claude.loopFindings.length + gemini.loopFindings.length + codex.loopFindings.length;
11516
- const totalCostUSD = (claude.totalCostUSD || 0) + (gemini.totalCostUSD || 0) + (codex.totalCostUSD || 0);
11620
+ const summary = buildScanSummary([
11621
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claude },
11622
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: gemini },
11623
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codex }
11624
+ ]);
11517
11625
  res.writeHead(200, { "Content-Type": "application/json" });
11518
- return res.end(
11519
- JSON.stringify({
11520
- status: "complete",
11521
- summary: {
11522
- sessions: totalSessions,
11523
- totalCostUSD,
11524
- findings: totalFindings,
11525
- dlp: totalDlp,
11526
- loops: totalLoops
11527
- },
11528
- sources
11529
- })
11530
- );
11626
+ return res.end(JSON.stringify({ status: "complete", summary }));
11531
11627
  } catch (err2) {
11532
11628
  console.error("Scan failed:", err2);
11533
11629
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -11866,6 +11962,7 @@ var init_server = __esm({
11866
11962
  init_shields();
11867
11963
  init_ui2();
11868
11964
  init_scan();
11965
+ init_scan_summary();
11869
11966
  init_state2();
11870
11967
  init_state();
11871
11968
  init_patch();
@@ -20520,10 +20617,10 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
20520
20617
  program.help();
20521
20618
  return;
20522
20619
  }
20523
- const fullCommand2 = runArgs.join(" ");
20620
+ const fullCommand = runArgs.join(" ");
20524
20621
  let result = await authorizeHeadless(
20525
20622
  "shell",
20526
- { command: fullCommand2 },
20623
+ { command: fullCommand },
20527
20624
  {
20528
20625
  agent: "Terminal"
20529
20626
  }
@@ -20531,11 +20628,11 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
20531
20628
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
20532
20629
  console.error(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
20533
20630
  const daemonReady = await autoStartDaemonAndWait();
20534
- if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand2 });
20631
+ if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
20535
20632
  }
20536
20633
  if (result.noApprovalMechanism && process.stdout.isTTY) {
20537
20634
  const approved = await confirm2({
20538
- message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand2}"?`,
20635
+ message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
20539
20636
  default: false
20540
20637
  });
20541
20638
  result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
@@ -20548,7 +20645,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
20548
20645
  process.exit(1);
20549
20646
  }
20550
20647
  console.error(chalk26.green("\n\u2705 Approved \u2014 running command...\n"));
20551
- await runProxy(fullCommand2);
20648
+ await runProxy(fullCommand);
20552
20649
  } else {
20553
20650
  program.help();
20554
20651
  }