@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.js CHANGED
@@ -5735,6 +5735,12 @@ var init_ui = __esm({
5735
5735
  text-align: center;
5736
5736
  }
5737
5737
  /* results */
5738
+ .scan-date-range {
5739
+ text-align: center;
5740
+ font-size: 11px;
5741
+ color: var(--muted);
5742
+ padding: 6px 0 10px;
5743
+ }
5738
5744
  .scan-summary-row {
5739
5745
  display: grid;
5740
5746
  grid-template-columns: repeat(4, 1fr);
@@ -7627,13 +7633,10 @@ var init_ui = __esm({
7627
7633
  const res = await fetch('/scan', { headers: { 'X-Node9-Token': CSRF_TOKEN } });
7628
7634
  if (!res.ok) return;
7629
7635
  const data = await res.json();
7630
- if (data.status !== 'complete') return;
7636
+ if (data.status !== 'complete' || !data.summary) return;
7631
7637
  _scanData = data;
7632
- const sources = data.sources || [];
7633
- const total =
7634
- sources.reduce((n, s) => n + (s.findings || []).length, 0) +
7635
- sources.reduce((n, s) => n + (s.leaks || []).length, 0) +
7636
- sources.reduce((n, s) => n + (s.loops || []).length, 0);
7638
+ const v = data.summary.byVerdict || {};
7639
+ const total = (v.blocked || 0) + (v.supervised || 0) + (v.leaks || 0) + (v.loops || 0);
7637
7640
  if (total > 0) {
7638
7641
  const btn = document.getElementById('btn-scan-history');
7639
7642
  if (btn && !btn.querySelector('.scan-badge')) {
@@ -7738,42 +7741,26 @@ var init_ui = __esm({
7738
7741
  }
7739
7742
 
7740
7743
  function renderScanResults(data) {
7741
- if (!data || data.status !== 'complete') {
7744
+ if (!data || data.status !== 'complete' || !data.summary) {
7742
7745
  return '<div style="text-align:center;color:var(--muted);padding:32px">No data returned.</div>';
7743
7746
  }
7744
- const s = data.summary || {};
7745
- const sources = data.sources || [];
7746
-
7747
- // Flatten all findings across sources
7748
- const allFindings = sources.flatMap((src) => src.findings || []);
7749
- const allLeaks = sources.flatMap((src) => src.leaks || []);
7750
- const allLoops = sources.flatMap((src) => src.loops || []);
7751
-
7752
- // Split findings into three distinct buckets
7753
- const blocked = allFindings.filter((f) => f.verdict === 'block' && f.ruleSource !== 'user');
7754
- const supervised = allFindings.filter(
7755
- (f) => f.verdict !== 'block' && f.ruleSource !== 'user'
7756
- );
7757
- const yourRules = allFindings.filter((f) => f.ruleSource === 'user');
7758
-
7759
- // Loop cost estimate: each wasted iteration ~2K tokens at Sonnet pricing
7760
- const LOOP_THRESHOLD = 3;
7761
- const COST_PER_ITER = 0.006;
7762
- const wastedIters = allLoops.reduce(
7763
- (sum, l) => sum + Math.max(0, (l.count || 0) - LOOP_THRESHOLD),
7764
- 0
7765
- );
7766
- const loopWastedUSD = wastedIters * COST_PER_ITER;
7767
-
7768
- const totalCost = s.totalCostUSD || 0;
7747
+ // Server-computed ScanSummary (see src/scan-summary.ts).
7748
+ // This renderer is a pure presentation layer \u2014 no categorization here.
7749
+ const summary = data.summary;
7750
+ const stats = summary.stats || {};
7751
+ const byVerdict = summary.byVerdict || { blocked: 0, supervised: 0, leaks: 0, loops: 0 };
7752
+ const byAgent = summary.byAgent || [];
7753
+ const sections = summary.sections || [];
7754
+ const leaks = summary.leaks || [];
7755
+ const loops = summary.loops || [];
7756
+ const loopWastedUSD = summary.loopWastedUSD || 0;
7757
+ const totalCost = stats.totalCostUSD || 0;
7769
7758
 
7770
7759
  function fmtUSD(n) {
7771
7760
  if (!n || n < 0.001) return null;
7772
7761
  if (n < 1) return '$' + n.toFixed(3);
7773
7762
  return '$' + n.toFixed(2);
7774
7763
  }
7775
-
7776
- // \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
7777
7764
  const costStr = fmtUSD(totalCost);
7778
7765
  const savingsStr = fmtUSD(loopWastedUSD);
7779
7766
 
@@ -7795,10 +7782,46 @@ var init_ui = __esm({
7795
7782
  );
7796
7783
  }
7797
7784
 
7798
- const stats =
7785
+ // \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
7786
+ function fmtShort(iso) {
7787
+ if (!iso) return null;
7788
+ try {
7789
+ return new Date(iso).toLocaleDateString([], {
7790
+ month: 'short',
7791
+ day: 'numeric',
7792
+ year: 'numeric',
7793
+ });
7794
+ } catch {
7795
+ return null;
7796
+ }
7797
+ }
7798
+ const firstLabel = fmtShort(stats.firstDate);
7799
+ const lastLabel = fmtShort(stats.lastDate);
7800
+ let daySpan = '';
7801
+ if (stats.firstDate && stats.lastDate) {
7802
+ const days =
7803
+ Math.round(
7804
+ (new Date(stats.lastDate).getTime() - new Date(stats.firstDate).getTime()) /
7805
+ (1000 * 60 * 60 * 24)
7806
+ ) + 1;
7807
+ if (days > 1) daySpan = ' \xB7 ' + days + ' day' + (days !== 1 ? 's' : '');
7808
+ }
7809
+ const dateRangeHtml =
7810
+ firstLabel && lastLabel
7811
+ ? '<div class="scan-date-range">\u{1F4C5} ' +
7812
+ esc(firstLabel) +
7813
+ ' \u2013 ' +
7814
+ esc(lastLabel) +
7815
+ esc(daySpan) +
7816
+ '</div>'
7817
+ : '';
7818
+
7819
+ // \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
7820
+ const statsHtml =
7821
+ dateRangeHtml +
7799
7822
  '<div class="scan-summary-row">' +
7800
7823
  '<div class="scan-stat">' +
7801
- statVal(s.sessions || 0, '') +
7824
+ statVal(stats.sessions || 0, '') +
7802
7825
  '<div class="scan-stat-label">Sessions</div></div>' +
7803
7826
  (totalCost > 0
7804
7827
  ? '<div class="scan-stat">' +
@@ -7806,19 +7829,16 @@ var init_ui = __esm({
7806
7829
  '<div class="scan-stat-label">AI Spend</div></div>'
7807
7830
  : '') +
7808
7831
  '<div class="scan-stat">' +
7809
- statVal(blocked.length, blocked.length ? 'danger' : 'ok') +
7832
+ statVal(byVerdict.blocked || 0, byVerdict.blocked ? 'danger' : 'ok') +
7810
7833
  '<div class="scan-stat-label">Blocked</div></div>' +
7811
7834
  '<div class="scan-stat">' +
7812
- statVal(supervised.length, supervised.length ? 'warning' : 'ok') +
7835
+ statVal(byVerdict.supervised || 0, byVerdict.supervised ? 'warning' : 'ok') +
7813
7836
  '<div class="scan-stat-label">Supervised</div></div>' +
7814
7837
  '<div class="scan-stat">' +
7815
- statVal(yourRules.length, '', yourRules.length ? 'color:#58a6ff' : '') +
7816
- '<div class="scan-stat-label">Your Rules</div></div>' +
7817
- '<div class="scan-stat">' +
7818
- statVal(allLeaks.length, allLeaks.length ? 'danger' : 'ok') +
7838
+ statVal(byVerdict.leaks || 0, byVerdict.leaks ? 'danger' : 'ok') +
7819
7839
  '<div class="scan-stat-label">Leaks</div></div>' +
7820
7840
  '<div class="scan-stat">' +
7821
- statVal(allLoops.length, allLoops.length ? 'warning' : 'ok') +
7841
+ statVal(byVerdict.loops || 0, byVerdict.loops ? 'warning' : 'ok') +
7822
7842
  '<div class="scan-stat-label">Loops</div></div>' +
7823
7843
  '</div>' +
7824
7844
  (savingsStr
@@ -7827,56 +7847,103 @@ var init_ui = __esm({
7827
7847
  '</span> in unnecessary LLM turns</div>'
7828
7848
  : '');
7829
7849
 
7830
- if (!allFindings.length && !allLeaks.length && !allLoops.length) {
7850
+ const hasNothing = !sections.length && !leaks.length && !loops.length;
7851
+ if (hasNothing) {
7831
7852
  return (
7832
- stats +
7853
+ statsHtml +
7833
7854
  '<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 ' +
7834
- (s.sessions || 0) +
7835
- ' sessions across ' +
7836
- sources.length +
7837
- ' AI source(s)</span></div>'
7855
+ (stats.sessions || 0) +
7856
+ ' sessions</span></div>'
7838
7857
  );
7839
7858
  }
7840
7859
 
7841
- // \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
7842
- function groupByKey(items, keyFn) {
7843
- const map = new Map();
7844
- for (const item of items) {
7845
- const k = keyFn(item);
7846
- if (!map.has(k)) map.set(k, []);
7847
- map.get(k).push(item);
7860
+ // \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
7861
+ const agentBreakdown = byAgent.length
7862
+ ? '<div class="scan-agent-group">' +
7863
+ byAgent
7864
+ .map(
7865
+ (a) =>
7866
+ '<div class="scan-agent-row">' +
7867
+ '<span class="scan-agent-name">' +
7868
+ esc(a.icon || '') +
7869
+ ' ' +
7870
+ esc(a.label) +
7871
+ '</span>' +
7872
+ '<span class="scan-agent-stat">' +
7873
+ a.sessions +
7874
+ ' session' +
7875
+ (a.sessions !== 1 ? 's' : '') +
7876
+ '</span>' +
7877
+ '<span class="scan-agent-sep">\xB7</span>' +
7878
+ '<span class="scan-agent-stat">' +
7879
+ a.findings +
7880
+ ' finding' +
7881
+ (a.findings !== 1 ? 's' : '') +
7882
+ '</span>' +
7883
+ '</div>'
7884
+ )
7885
+ .join('') +
7886
+ '</div>'
7887
+ : '';
7888
+
7889
+ // \u2500\u2500 Rule row renderer \u2014 shared between rule sections + leaks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7890
+ // Determines bar-max across all sections so relative counts stay honest.
7891
+ let maxBar = 1;
7892
+ for (const section of sections) {
7893
+ for (const rule of section.rules || []) {
7894
+ if (rule.findings.length > maxBar) maxBar = rule.findings.length;
7848
7895
  }
7849
- return [...map.entries()].sort((a, b) => b[1].length - a[1].length);
7850
7896
  }
7897
+ // Group leaks by patternName for display
7898
+ const leaksByPattern = (function () {
7899
+ const m = new Map();
7900
+ for (const l of leaks) {
7901
+ const k = l.patternName || 'DLP';
7902
+ if (!m.has(k)) m.set(k, []);
7903
+ m.get(k).push(l);
7904
+ }
7905
+ for (const [, group] of m) if (group.length > maxBar) maxBar = group.length;
7906
+ return [...m.entries()].sort((a, b) => b[1].length - a[1].length);
7907
+ })();
7851
7908
 
7852
- function findingRow(item, type) {
7853
- const date = new Date(item.timestamp).toLocaleDateString([], {
7909
+ function findingRow(timestamp, text) {
7910
+ const date = new Date(timestamp).toLocaleDateString([], {
7854
7911
  month: 'short',
7855
7912
  day: 'numeric',
7856
7913
  });
7857
- const cmdText =
7858
- type === 'leak' ? esc(item.sample || '') : esc(truncate(item.command || '', 120));
7859
7914
  return (
7860
7915
  '<div class="scan-finding-row"><span class="scan-finding-ts">' +
7861
7916
  date +
7862
7917
  '</span><span class="scan-finding-cmd">' +
7863
- cmdText +
7918
+ text +
7864
7919
  '</span></div>'
7865
7920
  );
7866
7921
  }
7867
7922
 
7868
- function ruleSection(items, type, badgeClass, badgeLabel, maxBar) {
7869
- const keyFn =
7870
- type === 'leak'
7871
- ? (i) => i.pattern || 'DLP'
7872
- : (i) => (i.rule || 'unknown').replace(/^shield:[^:]+:/, '');
7873
- const grouped = groupByKey(items, keyFn);
7874
- return grouped
7875
- .map(([rule, group]) => {
7876
- const count = group.length;
7923
+ function ruleCardsForSection(section) {
7924
+ return (section.rules || [])
7925
+ .map((rule) => {
7926
+ const count = rule.findings.length;
7877
7927
  const barPct = Math.round((count / maxBar) * 100);
7878
7928
  const detailId = 'detail-' + Math.random().toString(36).slice(2);
7879
- const rows = group.map((i) => findingRow(i, type)).join('');
7929
+ const badgeClass =
7930
+ section.sourceType === 'user'
7931
+ ? 'badge-user'
7932
+ : rule.verdict === 'block'
7933
+ ? 'badge-block'
7934
+ : 'badge-review';
7935
+ const badgeLabel =
7936
+ section.sourceType === 'user'
7937
+ ? rule.verdict === 'block'
7938
+ ? 'YOUR BLOCK'
7939
+ : 'YOUR RULE'
7940
+ : rule.verdict === 'block'
7941
+ ? 'BLOCK'
7942
+ : 'REVIEW';
7943
+ const barColor = section.sourceType === 'user' ? ';background:#388bfd33' : '';
7944
+ const rows = rule.findings
7945
+ .map((f) => findingRow(f.timestamp, esc(truncate(f.command || '', 120))))
7946
+ .join('');
7880
7947
  return (
7881
7948
  '<div class="scan-rule-row" onclick="var d=document.getElementById(\\'' +
7882
7949
  detailId +
@@ -7887,12 +7954,12 @@ var init_ui = __esm({
7887
7954
  badgeLabel +
7888
7955
  '</span>' +
7889
7956
  '<span class="scan-rule-name">' +
7890
- esc(rule) +
7957
+ esc(rule.name) +
7891
7958
  '</span>' +
7892
7959
  '<span class="scan-rule-bar-wrap"><span class="scan-rule-bar" style="width:' +
7893
7960
  barPct +
7894
7961
  '%' +
7895
- (badgeClass === 'badge-user' ? ';background:#388bfd33' : '') +
7962
+ barColor +
7896
7963
  '"></span></span>' +
7897
7964
  '<span class="scan-rule-count">' +
7898
7965
  count +
@@ -7908,98 +7975,95 @@ var init_ui = __esm({
7908
7975
  .join('');
7909
7976
  }
7910
7977
 
7911
- // \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
7912
- const maxBar = Math.max(
7913
- ...allFindings.map((f) => 1),
7914
- ...groupByKey(allFindings, (f) => (f.rule || '').replace(/^shield:[^:]+:/, '')).map(
7915
- ([, v]) => v.length
7916
- ),
7917
- ...groupByKey(allLeaks, (f) => f.pattern || 'DLP').map(([, v]) => v.length),
7918
- 1
7919
- );
7978
+ // \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
7979
+ function sectionHeading(section) {
7980
+ if (section.id === 'default') {
7981
+ return '\u{1F441} ' + esc(section.label) + ' \u2014 built-in, always on';
7982
+ }
7983
+ if (section.sourceType === 'shield') {
7984
+ return (
7985
+ '\u{1F6E1} ' +
7986
+ esc(section.label) +
7987
+ ' shield' +
7988
+ (section.subtitle ? ' \u2014 ' + esc(section.subtitle) : '')
7989
+ );
7990
+ }
7991
+ if (section.id === 'cloud') {
7992
+ return '\u2601 Cloud Policy \u2014 synced from node9';
7993
+ }
7994
+ return '\u2705 Your Rules \u2014 working as configured';
7995
+ }
7996
+ function sectionColor(section) {
7997
+ if (section.blockedCount > 0) return '#e5534b';
7998
+ if (section.sourceType === 'user') return '#58a6ff';
7999
+ return 'var(--muted)';
8000
+ }
7920
8001
 
7921
- // Per-agent breakdown \u2014 one row per active agent (only shows active ones)
7922
- const agentBreakdown = (function () {
7923
- const rows = sources
7924
- .filter(
7925
- (src) =>
7926
- (src.sessions || 0) > 0 ||
7927
- (src.findings || []).length > 0 ||
7928
- (src.leaks || []).length > 0 ||
7929
- (src.loops || []).length > 0
7930
- )
7931
- .map((src) => {
7932
- const f = (src.findings || []).length;
7933
- const l = (src.leaks || []).length;
7934
- const lp = (src.loops || []).length;
7935
- const findingsTotal = f + l + lp;
8002
+ let html = statsHtml + agentBreakdown;
8003
+
8004
+ // \u2500\u2500 Leaks (shown first: credential exposure is highest-severity) \u2500\u2500\u2500\u2500\u2500
8005
+ if (leaksByPattern.length) {
8006
+ html +=
8007
+ '<div class="scan-rule-section-label" style="color:#e5534b">\u{1F511} Credential Leaks \u2014 secrets found in tool calls</div>';
8008
+ html += leaksByPattern
8009
+ .map(([pattern, group]) => {
8010
+ const count = group.length;
8011
+ const barPct = Math.round((count / maxBar) * 100);
8012
+ const detailId = 'detail-' + Math.random().toString(36).slice(2);
8013
+ const rows = group
8014
+ .map((l) => findingRow(l.timestamp, esc(l.redactedSample || '')))
8015
+ .join('');
7936
8016
  return (
7937
- '<div class="scan-agent-row">' +
7938
- '<span class="scan-agent-name">' +
7939
- esc(src.icon || '') +
7940
- ' ' +
7941
- esc(src.label || src.id) +
7942
- '</span>' +
7943
- '<span class="scan-agent-stat">' +
7944
- (src.sessions || 0) +
7945
- ' session' +
7946
- ((src.sessions || 0) !== 1 ? 's' : '') +
8017
+ '<div class="scan-rule-row" onclick="var d=document.getElementById(\\'' +
8018
+ detailId +
8019
+ "');var a=this.querySelector('.scan-rule-arrow');d.style.display=d.style.display===''?'none':'';a.textContent=d.style.display===''?'\u25B2':'\u25BC';\\">" +
8020
+ '<span class="scan-verdict-badge badge-dlp">DLP</span>' +
8021
+ '<span class="scan-rule-name">' +
8022
+ esc(pattern) +
7947
8023
  '</span>' +
7948
- '<span class="scan-agent-sep">\xB7</span>' +
7949
- '<span class="scan-agent-stat">' +
7950
- findingsTotal +
7951
- ' finding' +
7952
- (findingsTotal !== 1 ? 's' : '') +
8024
+ '<span class="scan-rule-bar-wrap"><span class="scan-rule-bar" style="width:' +
8025
+ barPct +
8026
+ '%"></span></span>' +
8027
+ '<span class="scan-rule-count">' +
8028
+ count +
7953
8029
  '</span>' +
8030
+ '<span class="scan-rule-arrow">\u25BC</span>' +
8031
+ '</div><div id="' +
8032
+ detailId +
8033
+ '" style="display:none" class="scan-rule-detail">' +
8034
+ rows +
7954
8035
  '</div>'
7955
8036
  );
7956
- });
7957
- if (!rows.length) return '';
7958
- return '<div class="scan-agent-group">' + rows.join('') + '</div>';
7959
- })();
7960
-
7961
- let html = stats + agentBreakdown;
7962
-
7963
- if (allLeaks.length) {
7964
- html +=
7965
- '<div class="scan-rule-section-label" style="color:#e5534b">\u{1F511} Credential Leaks \u2014 secrets found in tool calls</div>';
7966
- html += ruleSection(allLeaks, 'leak', 'badge-dlp', 'DLP', maxBar);
7967
- }
7968
-
7969
- if (blocked.length) {
7970
- html +=
7971
- '<div class="scan-rule-section-label" style="color:#e5534b">\u{1F6D1} Blocked \u2014 Node9 would have stopped these</div>';
7972
- html += ruleSection(blocked, 'risk', 'badge-block', 'BLOCK', maxBar);
7973
- }
7974
-
7975
- if (supervised.length) {
7976
- html +=
7977
- '<div class="scan-rule-section-label">\u{1F441} Supervised \u2014 Node9 would have asked you first</div>';
7978
- html += ruleSection(supervised, 'risk', 'badge-review', 'REVIEW', maxBar);
8037
+ })
8038
+ .join('');
7979
8039
  }
7980
8040
 
7981
- if (yourRules.length) {
8041
+ // \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
8042
+ for (const section of sections) {
7982
8043
  html +=
7983
- '<div class="scan-rule-section-label" style="color:#58a6ff">\u2705 Your Rules \u2014 working as configured</div>';
7984
- html += ruleSection(yourRules, 'risk', 'badge-user', 'YOUR RULE', maxBar);
8044
+ '<div class="scan-rule-section-label" style="color:' +
8045
+ sectionColor(section) +
8046
+ '">' +
8047
+ sectionHeading(section) +
8048
+ '</div>';
8049
+ html += ruleCardsForSection(section);
7985
8050
  }
7986
8051
 
7987
- // Loops: single collapsed summary card, expand to see all groups
7988
- if (allLoops.length) {
7989
- const totalReps = allLoops.reduce((sum, l) => sum + (l.count || 0), 0);
8052
+ // \u2500\u2500 Loops: single collapsed summary card, expand to see all groups \u2500\u2500\u2500
8053
+ if (loops.length) {
8054
+ const totalReps = loops.reduce((sum, l) => sum + (l.count || 0), 0);
7990
8055
  const loopsDetailId = 'loops-detail-' + Math.random().toString(36).slice(2);
7991
8056
  const savingNote = savingsStr
7992
8057
  ? ' \xB7 <span style="color:var(--warning)">~' + savingsStr + ' wasted</span>'
7993
8058
  : '';
7994
- const loopGroupRows = allLoops
8059
+ const maxLoopCount = Math.max(...loops.map((l) => l.count || 0), 1);
8060
+ const loopGroupRows = loops
7995
8061
  .map((item) => {
7996
8062
  const date = new Date(item.timestamp).toLocaleDateString([], {
7997
8063
  month: 'short',
7998
8064
  day: 'numeric',
7999
8065
  });
8000
- const barPct = Math.round(
8001
- (item.count / Math.max(...allLoops.map((l) => l.count), 1)) * 100
8002
- );
8066
+ const barPct = Math.round((item.count / maxLoopCount) * 100);
8003
8067
  const detailId = 'detail-' + Math.random().toString(36).slice(2);
8004
8068
  return (
8005
8069
  '<div class="scan-rule-row" onclick="event.stopPropagation();var d=document.getElementById(\\'' +
@@ -8037,9 +8101,9 @@ var init_ui = __esm({
8037
8101
  "');var a=this.querySelector('.scan-rule-arrow');d.style.display=d.style.display===''?'none':'';a.textContent=d.style.display===''?'\u25B2':'\u25BC';\\">" +
8038
8102
  '<span class="scan-verdict-badge badge-review">LOOP</span>' +
8039
8103
  '<span class="scan-rule-name">' +
8040
- allLoops.length +
8104
+ loops.length +
8041
8105
  ' pattern' +
8042
- (allLoops.length !== 1 ? 's' : '') +
8106
+ (loops.length !== 1 ? 's' : '') +
8043
8107
  ' \xB7 ' +
8044
8108
  totalReps +
8045
8109
  ' total repetitions' +
@@ -8113,10 +8177,20 @@ var init_ui = __esm({
8113
8177
  const data = await res.json();
8114
8178
  results.style.display = 'block';
8115
8179
 
8116
- if (data.status === 'complete') {
8117
- const sources = data.sources || [];
8118
- const allRisks = sources.flatMap((s) => s.findings || []);
8119
- const allLeaks = sources.flatMap((s) => s.leaks || []);
8180
+ if (data.status === 'complete' && data.summary) {
8181
+ const summary = data.summary;
8182
+ const byVerdict = summary.byVerdict || {};
8183
+ const sections = summary.sections || [];
8184
+ const leaks = summary.leaks || [];
8185
+ // Flatten rule findings across all sections so we can show a Top-N list
8186
+ const allRisks = [];
8187
+ for (const section of sections) {
8188
+ for (const rule of section.rules || []) {
8189
+ for (const f of rule.findings) {
8190
+ allRisks.push({ rule: rule.name, verdict: rule.verdict, ...f });
8191
+ }
8192
+ }
8193
+ }
8120
8194
 
8121
8195
  let risksHtml = '';
8122
8196
  if (allRisks.length > 0) {
@@ -8129,7 +8203,7 @@ var init_ui = __esm({
8129
8203
  (r) => \`
8130
8204
  <div class="finding-item">
8131
8205
  <span class="finding-ts">\${new Date(r.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
8132
- <span class="finding-rule">\${r.rule.replace(/^shield:[^:]+:/, '')}</span>
8206
+ <span class="finding-rule">\${esc(r.rule)}</span>
8133
8207
  <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>
8134
8208
  </div>
8135
8209
  \`
@@ -8140,18 +8214,18 @@ var init_ui = __esm({
8140
8214
  }
8141
8215
 
8142
8216
  let leaksHtml = '';
8143
- if (allLeaks.length > 0) {
8217
+ if (leaks.length > 0) {
8144
8218
  leaksHtml = \`
8145
8219
  <div class="findings-list" style="border-color: rgba(201, 60, 55, 0.3);">
8146
8220
  <div style="font-size: 10px; font-weight: 700; color: #f87171; margin-bottom: 8px; text-transform: uppercase;">Credential Leaks Identified</div>
8147
- \${allLeaks
8221
+ \${leaks
8148
8222
  .slice(0, 5)
8149
8223
  .map(
8150
8224
  (l) => \`
8151
8225
  <div class="finding-item">
8152
8226
  <span class="finding-ts">\${new Date(l.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
8153
- <span class="finding-rule">\${esc(l.pattern)}</span>
8154
- <span class="finding-cmd"><span class="finding-badge badge-leak">\u{1F511}</span>\${esc(l.sample)}</span>
8227
+ <span class="finding-rule">\${esc(l.patternName)}</span>
8228
+ <span class="finding-cmd"><span class="finding-badge badge-leak">\u{1F511}</span>\${esc(l.redactedSample)}</span>
8155
8229
  </div>
8156
8230
  \`
8157
8231
  )
@@ -8160,10 +8234,11 @@ var init_ui = __esm({
8160
8234
  \`;
8161
8235
  }
8162
8236
 
8237
+ const riskCount = (byVerdict.blocked || 0) + (byVerdict.supervised || 0);
8163
8238
  results.innerHTML = \`
8164
8239
  <div class="insight-hint" style="color: #57ab5a; border-color: rgba(87, 171, 90, 0.4); background: rgba(87, 171, 90, 0.08);">
8165
8240
  <strong>\u2705 History Audit Complete</strong><br>
8166
- Scanned \${data.summary.sessions} sessions. Found <strong>\${data.summary.findings} risky operations</strong> and <strong>\${data.summary.dlp} secret leaks</strong>.
8241
+ Scanned \${summary.stats?.sessions || 0} sessions. Found <strong>\${riskCount} risky operations</strong> and <strong>\${byVerdict.leaks || 0} secret leaks</strong>.
8167
8242
  Node9 is now actively protecting you from these types of events.
8168
8243
  </div>
8169
8244
  \${leaksHtml}
@@ -8249,6 +8324,195 @@ var init_daemon_starter = __esm({
8249
8324
  }
8250
8325
  });
8251
8326
 
8327
+ // src/scan-summary.ts
8328
+ function buildScanSummary(agents) {
8329
+ const stats = {
8330
+ sessions: 0,
8331
+ totalToolCalls: 0,
8332
+ bashCalls: 0,
8333
+ totalCostUSD: 0,
8334
+ firstDate: null,
8335
+ lastDate: null
8336
+ };
8337
+ for (const a of agents) {
8338
+ stats.sessions += a.scan.sessions;
8339
+ stats.totalToolCalls += a.scan.totalToolCalls;
8340
+ stats.bashCalls += a.scan.bashCalls;
8341
+ stats.totalCostUSD += a.scan.totalCostUSD;
8342
+ if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
8343
+ stats.firstDate = a.scan.firstDate;
8344
+ }
8345
+ if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
8346
+ stats.lastDate = a.scan.lastDate;
8347
+ }
8348
+ }
8349
+ const allFindings = agents.flatMap((a) => a.scan.findings);
8350
+ const allLeaks = agents.flatMap(
8351
+ (a) => a.scan.dlpFindings.map((f) => ({
8352
+ patternName: f.patternName,
8353
+ redactedSample: f.redactedSample,
8354
+ toolName: f.toolName,
8355
+ timestamp: f.timestamp,
8356
+ project: f.project,
8357
+ sessionId: f.sessionId,
8358
+ agent: f.agent
8359
+ }))
8360
+ );
8361
+ const allLoops = agents.flatMap(
8362
+ (a) => a.scan.loopFindings.map((f) => ({
8363
+ toolName: f.toolName,
8364
+ commandPreview: f.commandPreview,
8365
+ count: f.count,
8366
+ timestamp: f.timestamp,
8367
+ project: f.project,
8368
+ sessionId: f.sessionId,
8369
+ agent: f.agent
8370
+ }))
8371
+ );
8372
+ const byVerdict = {
8373
+ blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
8374
+ supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
8375
+ leaks: allLeaks.length,
8376
+ loops: allLoops.length
8377
+ };
8378
+ const byAgent = agents.map((a) => ({
8379
+ id: a.id,
8380
+ label: a.label,
8381
+ icon: a.icon,
8382
+ sessions: a.scan.sessions,
8383
+ findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
8384
+ costUSD: a.scan.totalCostUSD
8385
+ })).filter((s) => s.sessions > 0 || s.findings > 0);
8386
+ const sections = buildSections(allFindings);
8387
+ const wastedIters = allLoops.reduce(
8388
+ (sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE),
8389
+ 0
8390
+ );
8391
+ const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
8392
+ return {
8393
+ stats,
8394
+ byVerdict,
8395
+ byAgent,
8396
+ sections,
8397
+ leaks: allLeaks,
8398
+ loops: allLoops,
8399
+ loopWastedUSD
8400
+ };
8401
+ }
8402
+ function buildSections(findings) {
8403
+ const sectionMap = /* @__PURE__ */ new Map();
8404
+ function ensureSection(id, label, subtitle, sourceType, shieldKey) {
8405
+ let s = sectionMap.get(id);
8406
+ if (!s) {
8407
+ s = {
8408
+ id,
8409
+ label,
8410
+ subtitle,
8411
+ sourceType,
8412
+ shieldKey,
8413
+ blockedCount: 0,
8414
+ reviewCount: 0,
8415
+ rules: []
8416
+ };
8417
+ sectionMap.set(id, s);
8418
+ }
8419
+ return s;
8420
+ }
8421
+ const ruleMap = /* @__PURE__ */ new Map();
8422
+ for (const f of findings) {
8423
+ const src = f.source;
8424
+ const sourceType = src.sourceType;
8425
+ const shieldName = src.shieldName;
8426
+ const verdict = src.rule.verdict === "block" ? "block" : "review";
8427
+ let sectionId;
8428
+ let sectionLabel;
8429
+ let sectionSubtitle;
8430
+ let shieldKey;
8431
+ if (sourceType === "default") {
8432
+ sectionId = "default";
8433
+ sectionLabel = "Default Rules";
8434
+ sectionSubtitle = "built-in, always on";
8435
+ } else if (sourceType === "shield") {
8436
+ sectionId = `shield:${shieldName}`;
8437
+ sectionLabel = shieldName;
8438
+ sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
8439
+ shieldKey = shieldName;
8440
+ } else if (shieldName === "cloud") {
8441
+ sectionId = "cloud";
8442
+ sectionLabel = "Cloud Policy";
8443
+ sectionSubtitle = "synced from node9 cloud";
8444
+ } else {
8445
+ sectionId = "user";
8446
+ sectionLabel = "Your Rules";
8447
+ sectionSubtitle = "added in node9.config.json";
8448
+ }
8449
+ const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
8450
+ const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
8451
+ const ruleKey = sectionId + "::" + ruleDisplayName;
8452
+ let rule = ruleMap.get(ruleKey);
8453
+ if (!rule) {
8454
+ rule = {
8455
+ name: ruleDisplayName,
8456
+ verdict,
8457
+ reason: src.rule.reason ?? "",
8458
+ findings: []
8459
+ };
8460
+ ruleMap.set(ruleKey, rule);
8461
+ section.rules.push(rule);
8462
+ }
8463
+ const cmdPreview = previewCommand(f.input, 120);
8464
+ const fullCmd = fullCommandOf(f.input);
8465
+ const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
8466
+ if (!isDupe) {
8467
+ rule.findings.push({
8468
+ timestamp: f.timestamp ?? "",
8469
+ command: cmdPreview,
8470
+ fullCommand: fullCmd,
8471
+ project: f.project,
8472
+ sessionId: f.sessionId,
8473
+ agent: f.agent,
8474
+ toolName: f.toolName
8475
+ });
8476
+ }
8477
+ if (verdict === "block") section.blockedCount++;
8478
+ else section.reviewCount++;
8479
+ }
8480
+ const sections = [...sectionMap.values()];
8481
+ sections.sort((a, b) => {
8482
+ const aTotal = a.blockedCount + a.reviewCount;
8483
+ const bTotal = b.blockedCount + b.reviewCount;
8484
+ if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
8485
+ return bTotal - aTotal;
8486
+ });
8487
+ for (const s of sections) {
8488
+ s.rules.sort((a, b) => {
8489
+ const aBlock = a.verdict === "block" ? 1 : 0;
8490
+ const bBlock = b.verdict === "block" ? 1 : 0;
8491
+ if (bBlock !== aBlock) return bBlock - aBlock;
8492
+ return b.findings.length - a.findings.length;
8493
+ });
8494
+ }
8495
+ return sections;
8496
+ }
8497
+ function previewCommand(input, max) {
8498
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
8499
+ const s = String(raw).replace(/\s+/g, " ").trim();
8500
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
8501
+ }
8502
+ function fullCommandOf(input) {
8503
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
8504
+ return String(raw).replace(/\s+/g, " ").trim();
8505
+ }
8506
+ var LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD;
8507
+ var init_scan_summary = __esm({
8508
+ "src/scan-summary.ts"() {
8509
+ "use strict";
8510
+ init_shields();
8511
+ LOOP_THRESHOLD_FOR_WASTE = 3;
8512
+ COST_PER_LOOP_ITER_USD = 6e-3;
8513
+ }
8514
+ });
8515
+
8252
8516
  // src/cli/commands/scan.ts
8253
8517
  function claudeModelPrice(model) {
8254
8518
  const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
@@ -8289,10 +8553,6 @@ function preview(input, max) {
8289
8553
  const s = String(cmd).replace(/\s+/g, " ").trim();
8290
8554
  return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
8291
8555
  }
8292
- function fullCommand(input) {
8293
- const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
8294
- return String(cmd).replace(/\s+/g, " ").trim();
8295
- }
8296
8556
  function detectLoops(calls, project, sessionId, agent) {
8297
8557
  const counts = /* @__PURE__ */ new Map();
8298
8558
  for (const call of calls) {
@@ -9063,26 +9323,32 @@ function printFindingRow(f, drillDown, showSessionId, previewWidth) {
9063
9323
  const ts = f.timestamp ? import_chalk2.default.dim(fmtTs(f.timestamp) + " ") : "";
9064
9324
  const proj = import_chalk2.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
9065
9325
  const agentBadge = f.agent === "gemini" ? import_chalk2.default.blue("[Gemini] ") : f.agent === "codex" ? import_chalk2.default.magenta("[Codex] ") : import_chalk2.default.cyan("[Claude] ");
9066
- const cmd = drillDown ? import_chalk2.default.gray(fullCommand(f.input)) : import_chalk2.default.gray(preview(f.input, previewWidth));
9326
+ let cmdText;
9327
+ if (drillDown) {
9328
+ cmdText = f.fullCommand;
9329
+ } else {
9330
+ cmdText = f.command;
9331
+ if (cmdText.length > previewWidth) cmdText = cmdText.slice(0, previewWidth - 1) + "\u2026";
9332
+ }
9333
+ const cmd = import_chalk2.default.gray(cmdText);
9067
9334
  const sessionSuffix = showSessionId && f.sessionId ? import_chalk2.default.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
9068
9335
  console.log(` ${ts}${proj}${agentBadge}${cmd}${sessionSuffix}`);
9069
9336
  }
9070
- function printRuleGroup(ruleFindings, topN, drillDown, previewWidth) {
9071
- const rule = ruleFindings[0].source.rule;
9072
- const ruleCount = ruleFindings.length;
9337
+ function printRuleGroup(rule, topN, drillDown, previewWidth) {
9338
+ const findings = rule.findings;
9339
+ const ruleCount = findings.length;
9073
9340
  const countBadge = ruleCount > 1 ? import_chalk2.default.white(` \xD7${ruleCount}`) : "";
9074
- const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
9075
- const icon = verdictIcon(rule.verdict ?? "review");
9341
+ const icon = verdictIcon(rule.verdict);
9076
9342
  console.log(
9077
- " " + icon + " " + import_chalk2.default.white(shortName) + countBadge + (rule.reason ? import_chalk2.default.dim(` \u2014 ${rule.reason}`) : "")
9343
+ " " + icon + " " + import_chalk2.default.white(rule.name) + countBadge + (rule.reason ? import_chalk2.default.dim(` \u2014 ${rule.reason}`) : "")
9078
9344
  );
9079
- const shown = drillDown ? ruleFindings : ruleFindings.slice(0, topN);
9345
+ const shown = drillDown ? findings : findings.slice(0, topN);
9080
9346
  for (const f of shown) {
9081
9347
  printFindingRow(f, drillDown, drillDown, previewWidth);
9082
9348
  }
9083
- if (!drillDown && ruleFindings.length > topN) {
9349
+ if (!drillDown && findings.length > topN) {
9084
9350
  console.log(
9085
- import_chalk2.default.dim(` \u2026 and ${ruleFindings.length - topN} more (--drill-down for full list)`)
9351
+ import_chalk2.default.dim(` \u2026 and ${findings.length - topN} more (--drill-down for full list)`)
9086
9352
  );
9087
9353
  }
9088
9354
  }
@@ -9129,6 +9395,11 @@ function registerScanCommand(program2) {
9129
9395
  (done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done)
9130
9396
  );
9131
9397
  const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
9398
+ const summary = buildScanSummary([
9399
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
9400
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
9401
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
9402
+ ]);
9132
9403
  process.stdout.write("\r" + " ".repeat(60) + "\r");
9133
9404
  if (scan.filesScanned === 0) {
9134
9405
  console.log(import_chalk2.default.yellow(" No session history found."));
@@ -9191,76 +9462,27 @@ function registerScanCommand(program2) {
9191
9462
  );
9192
9463
  }
9193
9464
  console.log("");
9194
- const sections = [];
9195
- const defaultFindings = scan.findings.filter((f) => f.source.sourceType === "default");
9196
- if (defaultFindings.length > 0) {
9197
- sections.push({
9198
- label: "Default Rules",
9199
- subtitle: "built-in, always on",
9200
- findings: defaultFindings
9201
- });
9202
- }
9203
- const byShield = /* @__PURE__ */ new Map();
9204
- for (const f of scan.findings.filter((f2) => f2.source.sourceType === "shield")) {
9205
- const arr = byShield.get(f.source.shieldName) ?? [];
9206
- arr.push(f);
9207
- byShield.set(f.source.shieldName, arr);
9208
- }
9209
- const shieldsWithFindings = [...byShield.entries()].sort(
9210
- (a, b) => b[1].length - a[1].length
9211
- );
9212
- for (const [shieldName, findings] of shieldsWithFindings) {
9213
- const description = SHIELDS[shieldName]?.description ?? "";
9214
- sections.push({
9215
- label: shieldName,
9216
- subtitle: description,
9217
- shieldKey: shieldName,
9218
- findings
9219
- });
9220
- }
9221
- const userFindings = scan.findings.filter(
9222
- (f) => f.source.sourceType === "user" || f.source.shieldName === "cloud"
9223
- );
9224
- if (userFindings.length > 0) {
9225
- sections.push({
9226
- label: "Your Rules",
9227
- subtitle: "added in node9.config.json",
9228
- findings: userFindings
9229
- });
9230
- }
9231
- for (const section of sections) {
9232
- const sectionBlocked = section.findings.filter(
9233
- (f) => f.source.rule.verdict === "block"
9234
- ).length;
9235
- const sectionReview = section.findings.length - sectionBlocked;
9465
+ for (const section of summary.sections) {
9236
9466
  const countParts = [];
9237
- if (sectionBlocked > 0) countParts.push(import_chalk2.default.red(`${sectionBlocked} blocked`));
9238
- if (sectionReview > 0) countParts.push(import_chalk2.default.yellow(`${sectionReview} review`));
9467
+ if (section.blockedCount > 0)
9468
+ countParts.push(import_chalk2.default.red(`${section.blockedCount} blocked`));
9469
+ if (section.reviewCount > 0)
9470
+ countParts.push(import_chalk2.default.yellow(`${section.reviewCount} review`));
9239
9471
  const countStr = countParts.join(import_chalk2.default.dim(" \xB7 "));
9240
9472
  const enableHint = section.shieldKey ? import_chalk2.default.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
9241
9473
  console.log(" " + import_chalk2.default.dim("\u2500".repeat(70)));
9242
9474
  console.log(
9243
9475
  " " + import_chalk2.default.bold(section.label) + (section.subtitle ? import_chalk2.default.dim(` \xB7 ${section.subtitle}`) : "") + " " + countStr + enableHint
9244
9476
  );
9245
- const byRule = /* @__PURE__ */ new Map();
9246
- for (const f of section.findings) {
9247
- const ruleKey = f.source.rule.name ?? "unnamed";
9248
- const arr = byRule.get(ruleKey) ?? [];
9249
- arr.push(f);
9250
- byRule.set(ruleKey, arr);
9251
- }
9252
- const sortedRules = [...byRule.entries()].sort((a, b) => {
9253
- const aBlock = a[1][0].source.rule.verdict === "block" ? 1 : 0;
9254
- const bBlock = b[1][0].source.rule.verdict === "block" ? 1 : 0;
9255
- if (bBlock !== aBlock) return bBlock - aBlock;
9256
- return b[1].length - a[1].length;
9257
- });
9258
- for (const [, ruleFindings] of sortedRules) {
9259
- printRuleGroup(ruleFindings, topN, drillDown, previewWidth);
9477
+ for (const rule of section.rules) {
9478
+ printRuleGroup(rule, topN, drillDown, previewWidth);
9260
9479
  }
9261
9480
  console.log("");
9262
9481
  }
9263
- const emptyShields = Object.keys(SHIELDS).filter((n) => !byShield.has(n)).sort();
9482
+ const activeShieldIds = new Set(
9483
+ summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
9484
+ );
9485
+ const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
9264
9486
  if (emptyShields.length > 0) {
9265
9487
  console.log(" " + import_chalk2.default.dim("\u2500".repeat(70)));
9266
9488
  console.log(
@@ -9370,78 +9592,12 @@ function registerScanCommand(program2) {
9370
9592
  const internalToken = getInternalToken();
9371
9593
  if (internalToken) {
9372
9594
  try {
9373
- const mapFindings = (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
- rule: f.source?.rule?.name ?? f.source?.shieldLabel ?? "unnamed",
9378
- command: f.input?.command ?? f.input?.cmd ?? f.input?.file_path ?? f.toolName ?? "unknown",
9379
- verdict: f.source?.rule?.verdict ?? f.source?.rule?.action ?? "review",
9380
- ruleSource: f.source?.sourceType ?? "default",
9381
- source: src
9382
- }))
9383
- );
9384
- const mapLeaks = (arr, src) => (
9385
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9386
- arr.map((f) => ({
9387
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
9388
- pattern: f.patternName || "DLP",
9389
- sample: f.redactedSample || "********",
9390
- source: src
9391
- }))
9392
- );
9393
- const mapLoops = (arr, src) => (
9394
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9395
- arr.map((f) => ({
9396
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
9397
- toolName: f.toolName || "unknown",
9398
- commandPreview: f.commandPreview || "",
9399
- count: f.count || 0,
9400
- source: src
9401
- }))
9402
- );
9403
- const sources = [
9404
- {
9405
- id: "claude",
9406
- label: "Claude",
9407
- icon: "\u{1F916}",
9408
- sessions: claudeScan.sessions,
9409
- findings: mapFindings(claudeScan.findings, "claude"),
9410
- leaks: mapLeaks(claudeScan.dlpFindings, "claude"),
9411
- loops: mapLoops(claudeScan.loopFindings, "claude")
9412
- },
9413
- {
9414
- id: "gemini",
9415
- label: "Gemini",
9416
- icon: "\u264A",
9417
- sessions: geminiScan.sessions,
9418
- findings: mapFindings(geminiScan.findings, "gemini"),
9419
- leaks: mapLeaks(geminiScan.dlpFindings, "gemini"),
9420
- loops: mapLoops(geminiScan.loopFindings, "gemini")
9421
- },
9422
- {
9423
- id: "codex",
9424
- label: "Codex",
9425
- icon: "\u{1F52E}",
9426
- sessions: codexScan.sessions,
9427
- findings: mapFindings(codexScan.findings, "codex"),
9428
- leaks: mapLeaks(codexScan.dlpFindings, "codex"),
9429
- loops: mapLoops(codexScan.loopFindings, "codex")
9430
- }
9431
- ].filter(
9432
- (s) => s.sessions > 0 || s.findings.length > 0 || s.leaks.length > 0 || s.loops.length > 0
9433
- );
9434
- const payload = {
9435
- status: "complete",
9436
- summary: {
9437
- sessions: scan.sessions,
9438
- findings: scan.findings.length,
9439
- dlp: scan.dlpFindings.length,
9440
- loops: scan.loopFindings.length,
9441
- totalCostUSD: scan.totalCostUSD
9442
- },
9443
- sources
9444
- };
9595
+ const summary2 = buildScanSummary([
9596
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
9597
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
9598
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
9599
+ ]);
9600
+ const payload = { status: "complete", summary: summary2 };
9445
9601
  await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
9446
9602
  method: "POST",
9447
9603
  headers: { "Content-Type": "application/json", "x-node9-internal": internalToken },
@@ -9472,6 +9628,7 @@ var init_scan = __esm({
9472
9628
  init_dlp();
9473
9629
  init_daemon();
9474
9630
  init_daemon_starter();
9631
+ init_scan_summary();
9475
9632
  CLAUDE_PRICING = {
9476
9633
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
9477
9634
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -11448,9 +11605,21 @@ data: ${JSON.stringify(item.data)}
11448
11605
  const d = /* @__PURE__ */ new Date();
11449
11606
  d.setDate(d.getDate() - 90);
11450
11607
  d.setHours(0, 0, 0, 0);
11451
- let claude = { sessions: 0, findings: [], dlpFindings: [], loopFindings: [] };
11452
- let gemini = { sessions: 0, findings: [], dlpFindings: [], loopFindings: [] };
11453
- let codex = { sessions: 0, findings: [], dlpFindings: [], loopFindings: [] };
11608
+ const EMPTY_SCAN = {
11609
+ filesScanned: 0,
11610
+ sessions: 0,
11611
+ totalToolCalls: 0,
11612
+ bashCalls: 0,
11613
+ findings: [],
11614
+ dlpFindings: [],
11615
+ loopFindings: [],
11616
+ totalCostUSD: 0,
11617
+ firstDate: null,
11618
+ lastDate: null
11619
+ };
11620
+ let claude = EMPTY_SCAN;
11621
+ let gemini = EMPTY_SCAN;
11622
+ let codex = EMPTY_SCAN;
11454
11623
  try {
11455
11624
  claude = scanClaudeHistory(d);
11456
11625
  } catch (e) {
@@ -11466,86 +11635,13 @@ data: ${JSON.stringify(item.data)}
11466
11635
  } catch (e) {
11467
11636
  console.error("Codex scan failed:", e);
11468
11637
  }
11469
- const mapFindings = (arr, src) => (
11470
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11471
- arr.map((f) => ({
11472
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
11473
- rule: f.source?.rule?.name ?? f.source?.shieldLabel ?? "unnamed",
11474
- command: f.input?.command ?? f.input?.cmd ?? f.input?.file_path ?? f.toolName ?? "unknown",
11475
- verdict: f.source?.rule?.verdict ?? f.source?.rule?.action ?? "review",
11476
- ruleSource: f.source?.sourceType ?? "default",
11477
- source: src
11478
- }))
11479
- );
11480
- const mapLeaks = (arr, src) => (
11481
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11482
- arr.map((f) => ({
11483
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
11484
- pattern: f.patternName || "DLP",
11485
- sample: f.redactedSample || "********",
11486
- source: src
11487
- }))
11488
- );
11489
- const mapLoops = (arr, src) => (
11490
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11491
- arr.map((f) => ({
11492
- timestamp: f.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
11493
- toolName: f.toolName || "unknown",
11494
- commandPreview: f.commandPreview || "",
11495
- count: f.count || 0,
11496
- source: src
11497
- }))
11498
- );
11499
- const sources = [
11500
- {
11501
- id: "claude",
11502
- label: "Claude",
11503
- icon: "\u{1F916}",
11504
- sessions: claude.sessions,
11505
- findings: mapFindings(claude.findings, "claude"),
11506
- leaks: mapLeaks(claude.dlpFindings, "claude"),
11507
- loops: mapLoops(claude.loopFindings, "claude")
11508
- },
11509
- {
11510
- id: "gemini",
11511
- label: "Gemini",
11512
- icon: "\u264A",
11513
- sessions: gemini.sessions,
11514
- findings: mapFindings(gemini.findings, "gemini"),
11515
- leaks: mapLeaks(gemini.dlpFindings, "gemini"),
11516
- loops: mapLoops(gemini.loopFindings, "gemini")
11517
- },
11518
- {
11519
- id: "codex",
11520
- label: "Codex",
11521
- icon: "\u{1F52E}",
11522
- sessions: codex.sessions,
11523
- findings: mapFindings(codex.findings, "codex"),
11524
- leaks: mapLeaks(codex.dlpFindings, "codex"),
11525
- loops: mapLoops(codex.loopFindings, "codex")
11526
- }
11527
- ].filter(
11528
- (s) => s.sessions > 0 || s.findings.length > 0 || s.leaks.length > 0 || s.loops.length > 0
11529
- );
11530
- const totalSessions = claude.sessions + gemini.sessions + codex.sessions;
11531
- const totalFindings = claude.findings.length + gemini.findings.length + codex.findings.length;
11532
- const totalDlp = claude.dlpFindings.length + gemini.dlpFindings.length + codex.dlpFindings.length;
11533
- const totalLoops = claude.loopFindings.length + gemini.loopFindings.length + codex.loopFindings.length;
11534
- const totalCostUSD = (claude.totalCostUSD || 0) + (gemini.totalCostUSD || 0) + (codex.totalCostUSD || 0);
11638
+ const summary = buildScanSummary([
11639
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claude },
11640
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: gemini },
11641
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codex }
11642
+ ]);
11535
11643
  res.writeHead(200, { "Content-Type": "application/json" });
11536
- return res.end(
11537
- JSON.stringify({
11538
- status: "complete",
11539
- summary: {
11540
- sessions: totalSessions,
11541
- totalCostUSD,
11542
- findings: totalFindings,
11543
- dlp: totalDlp,
11544
- loops: totalLoops
11545
- },
11546
- sources
11547
- })
11548
- );
11644
+ return res.end(JSON.stringify({ status: "complete", summary }));
11549
11645
  } catch (err2) {
11550
11646
  console.error("Scan failed:", err2);
11551
11647
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -11892,6 +11988,7 @@ var init_server = __esm({
11892
11988
  init_shields();
11893
11989
  init_ui2();
11894
11990
  init_scan();
11991
+ init_scan_summary();
11895
11992
  init_state2();
11896
11993
  init_state();
11897
11994
  init_patch();
@@ -20546,10 +20643,10 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
20546
20643
  program.help();
20547
20644
  return;
20548
20645
  }
20549
- const fullCommand2 = runArgs.join(" ");
20646
+ const fullCommand = runArgs.join(" ");
20550
20647
  let result = await authorizeHeadless(
20551
20648
  "shell",
20552
- { command: fullCommand2 },
20649
+ { command: fullCommand },
20553
20650
  {
20554
20651
  agent: "Terminal"
20555
20652
  }
@@ -20557,11 +20654,11 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
20557
20654
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
20558
20655
  console.error(import_chalk26.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
20559
20656
  const daemonReady = await autoStartDaemonAndWait();
20560
- if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand2 });
20657
+ if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
20561
20658
  }
20562
20659
  if (result.noApprovalMechanism && process.stdout.isTTY) {
20563
20660
  const approved = await (0, import_prompts2.confirm)({
20564
- message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand2}"?`,
20661
+ message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
20565
20662
  default: false
20566
20663
  });
20567
20664
  result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
@@ -20574,7 +20671,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
20574
20671
  process.exit(1);
20575
20672
  }
20576
20673
  console.error(import_chalk26.default.green("\n\u2705 Approved \u2014 running command...\n"));
20577
- await runProxy(fullCommand2);
20674
+ await runProxy(fullCommand);
20578
20675
  } else {
20579
20676
  program.help();
20580
20677
  }