@solongate/proxy 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui/index.js CHANGED
@@ -124,9 +124,8 @@ function KeyHints({ hints }) {
124
124
  ] }, i)) });
125
125
  }
126
126
 
127
- // src/tui/panels/Policies.tsx
128
- import { Box as Box2, Text as Text2, useInput } from "ink";
129
- import { useState as useState2 } from "react";
127
+ // src/tui/panels/Live.tsx
128
+ import { Box as Box2, Text as Text2 } from "ink";
130
129
 
131
130
  // src/api-client/client.ts
132
131
  import { readFileSync, existsSync } from "fs";
@@ -429,83 +428,335 @@ function usePoll(reload, intervalMs, enabled = true) {
429
428
  }, [reload, intervalMs, enabled]);
430
429
  }
431
430
 
432
- // src/tui/panels/Policies.tsx
431
+ // src/tui/panels/Live.tsx
433
432
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
433
+ function Tile({ label, value, color }) {
434
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginRight: 1, minWidth: 14, children: [
435
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label }),
436
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color, children: value })
437
+ ] });
438
+ }
439
+ function LivePanel({ active: active2 }) {
440
+ const stats = useLoader(() => api.stats.get());
441
+ const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
442
+ const feed = useLoader(() => api.audit.list({ limit: 12 }));
443
+ usePoll(() => {
444
+ stats.reload();
445
+ ts.reload();
446
+ feed.reload();
447
+ }, 2e3, active2);
448
+ const s = stats.data;
449
+ const points = ts.data?.timeseries ?? [];
450
+ const entries = feed.data?.entries ?? [];
451
+ const denyRate = s && s.total_calls > 0 ? Math.round(s.denied / s.total_calls * 100) : 0;
452
+ return /* @__PURE__ */ jsx2(DataView, { loading: stats.loading && !s, error: stats.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
453
+ /* @__PURE__ */ jsxs2(Box2, { children: [
454
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF" }),
455
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " LIVE \xB7 refreshing every 2s" })
456
+ ] }),
457
+ s ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
458
+ /* @__PURE__ */ jsx2(Tile, { label: "total calls", value: String(s.total_calls) }),
459
+ /* @__PURE__ */ jsx2(Tile, { label: "allowed", value: String(s.allowed), color: theme.ok }),
460
+ /* @__PURE__ */ jsx2(Tile, { label: "denied", value: String(s.denied), color: theme.bad }),
461
+ /* @__PURE__ */ jsx2(Tile, { label: "deny rate", value: denyRate + "%", color: denyRate > 10 ? theme.bad : void 0 }),
462
+ /* @__PURE__ */ jsx2(Tile, { label: "policies", value: String(s.active_policies), color: theme.accent })
463
+ ] }) : null,
464
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
465
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "Activity \xB7 24h" }),
466
+ /* @__PURE__ */ jsxs2(Box2, { children: [
467
+ /* @__PURE__ */ jsx2(Text2, { children: "calls " }),
468
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), 60) })
469
+ ] }),
470
+ /* @__PURE__ */ jsxs2(Box2, { children: [
471
+ /* @__PURE__ */ jsx2(Text2, { children: "denied " }),
472
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 60) })
473
+ ] })
474
+ ] }),
475
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
476
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "Live feed" }),
477
+ entries.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " waiting for tool calls\u2026" }) : entries.slice(0, 10).map((e) => /* @__PURE__ */ jsxs2(Box2, { children: [
478
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), children: e.decision.padEnd(8) }),
479
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 16).padEnd(17) }),
480
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent_name ?? "\u2014", 14).padEnd(15) }),
481
+ e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP " }) : /* @__PURE__ */ jsx2(Text2, { children: " " }),
482
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
483
+ ago(e.created_at),
484
+ " ago"
485
+ ] })
486
+ ] }, e.id))
487
+ ] })
488
+ ] }) });
489
+ }
490
+
491
+ // src/tui/panels/Policies.tsx
492
+ import { Box as Box3, Text as Text3, useInput } from "ink";
493
+ import TextInput from "ink-text-input";
494
+ import { useEffect as useEffect2, useState as useState2 } from "react";
495
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
496
+ var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
497
+ var setConstr = (r, g, side, val) => {
498
+ const arr = val.split(",").map((s) => s.trim()).filter(Boolean);
499
+ const prev = r[g] ?? {};
500
+ return { ...r, [g]: { ...prev, [side]: arr } };
501
+ };
502
+ var FIELDS = [
503
+ { label: "Effect", kind: "effect", get: (r) => r.effect },
504
+ { label: "Enabled", kind: "enabled", get: (r) => r.enabled ? "yes" : "no" },
505
+ { label: "Priority", kind: "priority", get: (r) => String(r.priority) },
506
+ { label: "Tool pattern", kind: "text", get: (r) => r.toolPattern, set: (r, v) => ({ ...r, toolPattern: v || "*" }) },
507
+ { label: "Description", kind: "text", get: (r) => r.description ?? "", set: (r, v) => ({ ...r, description: v }) },
508
+ { label: "Cmd allow", kind: "text", get: (r) => constr(r, "commandConstraints", "allowed"), set: (r, v) => setConstr(r, "commandConstraints", "allowed", v) },
509
+ { label: "Cmd deny", kind: "text", get: (r) => constr(r, "commandConstraints", "denied"), set: (r, v) => setConstr(r, "commandConstraints", "denied", v) },
510
+ { label: "Path allow", kind: "text", get: (r) => constr(r, "pathConstraints", "allowed"), set: (r, v) => setConstr(r, "pathConstraints", "allowed", v) },
511
+ { label: "Path deny", kind: "text", get: (r) => constr(r, "pathConstraints", "denied"), set: (r, v) => setConstr(r, "pathConstraints", "denied", v) },
512
+ { label: "File allow", kind: "text", get: (r) => constr(r, "filenameConstraints", "allowed"), set: (r, v) => setConstr(r, "filenameConstraints", "allowed", v) },
513
+ { label: "File deny", kind: "text", get: (r) => constr(r, "filenameConstraints", "denied"), set: (r, v) => setConstr(r, "filenameConstraints", "denied", v) },
514
+ { label: "URL allow", kind: "text", get: (r) => constr(r, "urlConstraints", "allowed"), set: (r, v) => setConstr(r, "urlConstraints", "allowed", v) },
515
+ { label: "URL deny", kind: "text", get: (r) => constr(r, "urlConstraints", "denied"), set: (r, v) => setConstr(r, "urlConstraints", "denied", v) }
516
+ ];
517
+ function ruleSummary(r) {
518
+ const bits = [];
519
+ for (const [g, tag] of [["commandConstraints", "cmd"], ["pathConstraints", "path"], ["filenameConstraints", "file"], ["urlConstraints", "url"]]) {
520
+ const c2 = r[g];
521
+ const n = (c2?.allowed?.length ?? 0) + (c2?.denied?.length ?? 0);
522
+ if (n) bits.push(`${tag}:${n}`);
523
+ }
524
+ return bits.join(" ");
525
+ }
434
526
  function PoliciesPanel({ focused }) {
435
- const [sel, setSel] = useState2(0);
436
527
  const list3 = useLoader(() => api.policies.list());
437
528
  const policies = list3.data?.policies ?? [];
438
- const selected = policies[Math.min(sel, Math.max(0, policies.length - 1))];
529
+ const [view, setView] = useState2("list");
530
+ const [pi, setPi] = useState2(0);
531
+ const [ri, setRi] = useState2(0);
532
+ const [fi, setFi] = useState2(0);
533
+ const [rules, setRules] = useState2([]);
534
+ const [mode, setMode] = useState2("denylist");
535
+ const [dirty, setDirty] = useState2(false);
536
+ const [editing, setEditing] = useState2(false);
537
+ const [editVal, setEditVal] = useState2("");
538
+ const [status, setStatus] = useState2(null);
539
+ const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
439
540
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
541
+ useEffect2(() => {
542
+ if (detail.data) {
543
+ setRules(detail.data.rules);
544
+ setMode(detail.data.mode ?? "denylist");
545
+ setDirty(false);
546
+ setRi(0);
547
+ }
548
+ }, [detail.data]);
549
+ const mutate = (nextRules, nextMode = mode) => {
550
+ setRules(nextRules);
551
+ setMode(nextMode);
552
+ setDirty(true);
553
+ setStatus(null);
554
+ };
555
+ const save = async () => {
556
+ if (!selected || !detail.data) return;
557
+ setStatus("Saving\u2026");
558
+ try {
559
+ const res = await api.policies.update(selected.id, {
560
+ id: selected.id,
561
+ name: detail.data.name,
562
+ description: detail.data.description,
563
+ mode,
564
+ agents: detail.data.agents,
565
+ rules
566
+ });
567
+ setRules(res.rules);
568
+ setDirty(false);
569
+ setStatus("\u2713 Saved v" + res._version);
570
+ list3.reload();
571
+ } catch (e) {
572
+ setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
573
+ }
574
+ };
575
+ const discard = () => {
576
+ if (detail.data) {
577
+ setRules(detail.data.rules);
578
+ setMode(detail.data.mode ?? "denylist");
579
+ }
580
+ setDirty(false);
581
+ setStatus("discarded");
582
+ };
440
583
  useInput(
441
- (_input, key) => {
442
- if (key.upArrow) setSel((n) => Math.max(0, n - 1));
443
- if (key.downArrow) setSel((n) => Math.min(policies.length - 1, n + 1));
584
+ (input, key) => {
585
+ if (view === "list") {
586
+ if (key.upArrow) setPi((n) => Math.max(0, n - 1));
587
+ else if (key.downArrow) setPi((n) => Math.min(policies.length - 1, n + 1));
588
+ else if (key.return || key.rightArrow) {
589
+ setStatus(null);
590
+ setView("rules");
591
+ }
592
+ return;
593
+ }
594
+ if (view === "rules") {
595
+ if (key.leftArrow) return setView("list"), void setStatus(null);
596
+ if (key.upArrow) setRi((n) => Math.max(0, n - 1));
597
+ else if (key.downArrow) setRi((n) => Math.min(rules.length - 1, n + 1));
598
+ else if (key.return || key.rightArrow) {
599
+ if (rules[ri]) {
600
+ setFi(0);
601
+ setView("rule");
602
+ }
603
+ } else if (input === " ") {
604
+ if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
605
+ } else if (input === "e") {
606
+ if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
607
+ } else if (input === "d") {
608
+ if (rules[ri]) {
609
+ const next = rules.filter((_, i) => i !== ri);
610
+ setRi((n) => Math.max(0, Math.min(n, next.length - 1)));
611
+ mutate(next);
612
+ }
613
+ } else if (input === "m") {
614
+ mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
615
+ } else if (input === "s") void save();
616
+ else if (input === "x") discard();
617
+ return;
618
+ }
619
+ const rule2 = rules[ri];
620
+ if (!rule2) return setView("rules");
621
+ const field = FIELDS[fi];
622
+ if (key.leftArrow) setView("rules");
623
+ else if (key.upArrow) setFi((n) => Math.max(0, n - 1));
624
+ else if (key.downArrow) setFi((n) => Math.min(FIELDS.length - 1, n + 1));
625
+ else if (field.kind === "effect" && (input === " " || key.rightArrow)) {
626
+ mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
627
+ } else if (field.kind === "enabled" && (input === " " || key.rightArrow)) {
628
+ mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
629
+ } else if (field.kind === "priority" && (key.rightArrow || key.leftArrow)) {
630
+ const d = key.rightArrow ? 1 : -1;
631
+ mutate(rules.map((r, i) => i === ri ? { ...r, priority: Math.max(0, r.priority + d) } : r));
632
+ } else if (field.kind === "text" && key.return) {
633
+ setEditVal(field.get(rule2));
634
+ setEditing(true);
635
+ } else if (input === "s") void save();
444
636
  },
445
- { isActive: focused }
637
+ { isActive: focused && !editing }
446
638
  );
447
- return /* @__PURE__ */ jsx2(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
448
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select policy" : "press \u2192 to browse" }),
449
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? theme.accentBright : void 0, bold: i === sel, children: [
450
- (i === sel ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
451
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
452
- p.mode.padEnd(10),
453
- " ",
454
- p.rules.length,
455
- " rules \xB7 v",
456
- p.version
457
- ] })
458
- ] }, p.id)) }),
459
- selected ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
460
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
461
- "Rules of ",
462
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: selected.id })
639
+ if (view === "list") {
640
+ return /* @__PURE__ */ jsx3(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
641
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
642
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs3(Text3, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
643
+ (i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
644
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
645
+ p.mode.padEnd(10),
646
+ " ",
647
+ p.rules.length,
648
+ " rules \xB7 v",
649
+ p.version
650
+ ] })
651
+ ] }, p.id)) })
652
+ ] }) });
653
+ }
654
+ const dirtyTag = dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null;
655
+ if (view === "rules") {
656
+ return /* @__PURE__ */ jsx3(DataView, { loading: detail.loading && !detail.data, error: detail.error, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
657
+ /* @__PURE__ */ jsxs3(Box3, { children: [
658
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 28) }),
659
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " mode: " }),
660
+ /* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
661
+ dirtyTag
463
662
  ] }),
464
- /* @__PURE__ */ jsx2(
663
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 rule \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 d delete \xB7 m mode \xB7 s save \xB7 \u2190 back" }),
664
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
465
665
  Table,
466
666
  {
467
667
  columns: [
668
+ { header: "", width: 2 },
669
+ { header: "ON", width: 2 },
468
670
  { header: "EFFECT", width: 7 },
469
671
  { header: "PRIO", width: 4 },
470
- { header: "TOOL", width: 22 },
471
- { header: "DESCRIPTION", width: 34 }
672
+ { header: "TOOL", width: 20 },
673
+ { header: "CONSTRAINTS", width: 22 }
472
674
  ],
473
- rows: (detail.data?.rules ?? []).slice(0, 10).map((r) => [
474
- { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad },
675
+ rows: rules.map((r, i) => [
676
+ { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
677
+ { value: r.enabled ? "\u25CF" : "\u25CB", color: r.enabled ? theme.ok : theme.dim },
678
+ { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
475
679
  { value: String(r.priority), dim: true },
476
- { value: truncate(r.toolPattern, 22), color: theme.accent },
477
- { value: truncate(r.description || "\u2014", 34) }
680
+ { value: truncate(r.toolPattern, 20), color: theme.accent, dim: !r.enabled },
681
+ { value: ruleSummary(r) || truncate(r.description || "\u2014", 22), dim: true }
478
682
  ])
479
683
  }
480
- )
481
- ] }) : null
482
- ] }) });
684
+ ) }),
685
+ rules.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "(no rules)" }) : null,
686
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
687
+ ] }) });
688
+ }
689
+ const rule = rules[ri];
690
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
691
+ /* @__PURE__ */ jsxs3(Box3, { children: [
692
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: "Rule " }),
693
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: truncate(rule?.id ?? "", 26) }),
694
+ dirtyTag
695
+ ] }),
696
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 field \xB7 enter edit text \xB7 space/\u2192 toggle \xB7 s save \xB7 \u2190 back" }),
697
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: FIELDS.map((f, i) => {
698
+ const val = rule ? f.get(rule) : "";
699
+ const active2 = i === fi;
700
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
701
+ /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + f.label.padEnd(13) }),
702
+ active2 && editing && f.kind === "text" ? /* @__PURE__ */ jsx3(
703
+ TextInput,
704
+ {
705
+ value: editVal,
706
+ onChange: setEditVal,
707
+ onSubmit: (v) => {
708
+ if (rule && f.set) mutate(rules.map((r, idx) => idx === ri ? f.set(r, v) : r));
709
+ setEditing(false);
710
+ }
711
+ }
712
+ ) : /* @__PURE__ */ jsx3(
713
+ Text3,
714
+ {
715
+ color: f.kind === "effect" ? val === "ALLOW" ? theme.ok : theme.bad : void 0,
716
+ dimColor: !val,
717
+ children: val || "\u2014"
718
+ }
719
+ )
720
+ ] }, f.label);
721
+ }) }),
722
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
723
+ ] });
483
724
  }
484
725
 
485
726
  // src/tui/panels/RateLimit.tsx
486
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
487
- import { useEffect as useEffect2, useState as useState3 } from "react";
488
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
727
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
728
+ import { useEffect as useEffect3, useState as useState3 } from "react";
729
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
489
730
  var MODES = ["off", "detect", "block"];
490
- var FIELDS = ["mode", "perMinute", "perHour", "perDay"];
731
+ var FIELDS2 = ["mode", "perMinute", "perHour", "perDay"];
732
+ var BLOCKS2 = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
733
+ function ColoredSpark({ values, limit, width = 60 }) {
734
+ const v = values.length > width ? values.slice(values.length - width) : values;
735
+ const max = Math.max(1, ...v);
736
+ return /* @__PURE__ */ jsx4(Text4, { children: v.map((n, i) => {
737
+ const ch = BLOCKS2[Math.min(BLOCKS2.length - 1, Math.round(n / max * (BLOCKS2.length - 1)))];
738
+ const over = limit > 0 && n > limit;
739
+ return /* @__PURE__ */ jsx4(Text4, { color: over ? theme.bad : theme.accent, children: ch }, i);
740
+ }) });
741
+ }
491
742
  function RateLimitPanel({ focused }) {
492
743
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
493
744
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
745
+ const insightsQ = useLoader(() => api.stats.securityInsights(7));
494
746
  const [draft, setDraft] = useState3(null);
495
747
  const [dirty, setDirty] = useState3(false);
496
748
  const [fi, setFi] = useState3(0);
497
749
  const [status, setStatus] = useState3(null);
498
- useEffect2(() => {
750
+ useEffect3(() => {
499
751
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
500
752
  }, [layersQ.data, draft]);
501
- const adjust = (dir) => {
753
+ const adjust = (dir, step) => {
502
754
  if (!draft) return;
503
- const field = FIELDS[fi];
755
+ const field = FIELDS2[fi];
504
756
  if (field === "mode") {
505
757
  const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
506
758
  setDraft({ ...draft, mode: MODES[idx] });
507
759
  } else {
508
- const step = 10;
509
760
  setDraft({ ...draft, [field]: Math.max(0, draft[field] + dir * step) });
510
761
  }
511
762
  setDirty(true);
@@ -515,8 +766,7 @@ function RateLimitPanel({ focused }) {
515
766
  if (!draft || !layersQ.data) return;
516
767
  setStatus("Saving\u2026");
517
768
  try {
518
- const next = { ...layersQ.data.layers, rateLimit: draft };
519
- const res = await api.settings.setSecurityLayers(next);
769
+ const res = await api.settings.setSecurityLayers({ ...layersQ.data.layers, rateLimit: draft });
520
770
  setDraft({ ...res.layers.rateLimit });
521
771
  setDirty(false);
522
772
  setStatus("\u2713 Saved");
@@ -527,119 +777,216 @@ function RateLimitPanel({ focused }) {
527
777
  };
528
778
  useInput2(
529
779
  (input, key) => {
530
- if (key.upArrow) setFi((n) => (n - 1 + FIELDS.length) % FIELDS.length);
531
- else if (key.downArrow) setFi((n) => (n + 1) % FIELDS.length);
532
- else if (key.leftArrow) adjust(-1);
533
- else if (key.rightArrow) adjust(1);
780
+ const step = key.shift ? 10 : 1;
781
+ if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
782
+ else if (key.downArrow) setFi((n) => (n + 1) % FIELDS2.length);
783
+ else if (key.leftArrow) adjust(-1, step);
784
+ else if (key.rightArrow) adjust(1, step);
534
785
  else if (input === "s") void save();
535
786
  },
536
787
  { isActive: focused }
537
788
  );
538
- const history = historyQ.data?.history ?? [];
539
- return /* @__PURE__ */ jsx3(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
540
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \u2190\u2192 change s save" : "press \u2192 to edit" }),
541
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
542
- /* @__PURE__ */ jsx3(Row, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
543
- /* @__PURE__ */ jsx3(Row, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
544
- /* @__PURE__ */ jsx3(Row, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
545
- /* @__PURE__ */ jsx3(Row, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
789
+ const history = (historyQ.data?.history ?? []).slice().sort((a, b) => b.ts - a.ts);
790
+ const activity = insightsQ.data?.["activity"] ?? {};
791
+ const minuteSeries = (activity.minute ?? []).map((b) => b.count);
792
+ const hourSeries = (activity.hour ?? []).map((b) => b.count);
793
+ const bursts = minuteSeries.filter((c2) => draft && draft.perMinute > 0 && c2 > draft.perMinute).length;
794
+ return /* @__PURE__ */ jsx4(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
795
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
796
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
797
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx4(Text4, { color: modeColor(draft.mode), children: draft.mode }) }),
798
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perMinute }) }),
799
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
800
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
546
801
  ] }),
547
- history.length ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
548
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "history " }),
549
- /* @__PURE__ */ jsx3(Text3, { color: theme.accent, children: sparkline(history.map((h) => h.minute), 40) }),
550
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " " + history.length + " changes" })
802
+ dirty || status ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
803
+ dirty ? /* @__PURE__ */ jsx4(Text4, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
804
+ status ? /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
551
805
  ] }) : null,
552
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
553
- dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
554
- status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
806
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
807
+ /* @__PURE__ */ jsxs4(Box4, { children: [
808
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Activity " }),
809
+ bursts > 0 ? /* @__PURE__ */ jsxs4(Text4, { color: theme.bad, children: [
810
+ bursts,
811
+ " burst min"
812
+ ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no bursts" }),
813
+ insightsQ.loading && !insightsQ.data ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " \u2026" }) : null
814
+ ] }),
815
+ /* @__PURE__ */ jsxs4(Box4, { children: [
816
+ /* @__PURE__ */ jsx4(Text4, { children: "/min " }),
817
+ minuteSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: minuteSeries, limit: draft.perMinute }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
818
+ ] }),
819
+ /* @__PURE__ */ jsxs4(Box4, { children: [
820
+ /* @__PURE__ */ jsx4(Text4, { children: "/hour " }),
821
+ hourSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: hourSeries, limit: draft.perHour, width: 24 }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
822
+ ] })
823
+ ] }),
824
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
825
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
826
+ "Change history ",
827
+ history.length ? `(${history.length})` : ""
828
+ ] }),
829
+ history.length ? /* @__PURE__ */ jsx4(
830
+ Table,
831
+ {
832
+ columns: [
833
+ { header: "WHEN", width: 10 },
834
+ { header: "/MIN", width: 6 },
835
+ { header: "/HOUR", width: 7 },
836
+ { header: "/DAY", width: 6 }
837
+ ],
838
+ rows: history.slice(0, 4).map((h, i) => [
839
+ { value: ago(h.ts) + " ago", dim: true, bold: i === 0 },
840
+ { value: String(h.minute), bold: i === 0 },
841
+ { value: h.hour === 0 ? "off" : String(h.hour), dim: h.hour === 0 },
842
+ { value: h.day === 0 ? "off" : String(h.day), dim: h.day === 0 }
843
+ ])
844
+ }
845
+ ) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " no changes" })
555
846
  ] })
556
847
  ] }) : null });
557
848
  }
558
- function Row({ label, active: active2, children }) {
559
- return /* @__PURE__ */ jsxs3(Box3, { children: [
560
- /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
849
+ function FieldRow({ label, active: active2, children }) {
850
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
851
+ /* @__PURE__ */ jsx4(Text4, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
561
852
  children
562
853
  ] });
563
854
  }
564
855
 
565
856
  // src/tui/panels/Dlp.tsx
566
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
567
- import { useEffect as useEffect3, useState as useState4 } from "react";
568
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
857
+ import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
858
+ import TextInput2 from "ink-text-input";
859
+ import { useEffect as useEffect4, useState as useState4 } from "react";
860
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
569
861
  var MODES2 = ["off", "detect", "block"];
570
862
  function DlpPanel({ focused }) {
571
863
  const q = useLoader(() => api.settings.getSecurityLayers());
572
864
  const [dlp, setDlp] = useState4(null);
573
865
  const [available, setAvailable] = useState4([]);
574
866
  const [sel, setSel] = useState4(0);
867
+ const [dirty, setDirty] = useState4(false);
575
868
  const [status, setStatus] = useState4(null);
576
- useEffect3(() => {
869
+ const [adding, setAdding] = useState4(null);
870
+ const [newName, setNewName] = useState4("");
871
+ const [newRe, setNewRe] = useState4("");
872
+ useEffect4(() => {
577
873
  if (q.data && !dlp) {
578
- setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns] });
874
+ setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
579
875
  setAvailable(q.data.availablePatterns);
580
876
  }
581
877
  }, [q.data, dlp]);
582
- const persist = async (nextDlp) => {
583
- if (!q.data) return;
584
- setDlp(nextDlp);
878
+ const mutate = (next) => {
879
+ setDlp(next);
880
+ setDirty(true);
881
+ setStatus(null);
882
+ };
883
+ const save = async () => {
884
+ if (!dlp || !q.data) return;
585
885
  setStatus("Saving\u2026");
586
886
  try {
587
- const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp: nextDlp });
588
- setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns] });
887
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
888
+ setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
889
+ setDirty(false);
589
890
  setStatus("\u2713 Saved");
590
891
  } catch (e) {
591
892
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
592
893
  }
593
894
  };
895
+ const discard = () => {
896
+ if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
897
+ setDirty(false);
898
+ setStatus("discarded");
899
+ };
900
+ const customCount = dlp?.custom.length ?? 0;
901
+ const total = available.length + customCount;
594
902
  useInput3(
595
903
  (input, key) => {
596
904
  if (!dlp) return;
597
905
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
598
- else if (key.downArrow) setSel((n) => Math.min(available.length - 1, n + 1));
599
- else if (input === " " || key.return) {
906
+ else if (key.downArrow) setSel((n) => Math.min(total - 1, n + 1));
907
+ else if (input === "m") {
908
+ const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
909
+ mutate({ ...dlp, mode: MODES2[idx] });
910
+ } else if (input === "a") {
911
+ setNewName("");
912
+ setNewRe("");
913
+ setAdding("name");
914
+ } else if (input === " " && sel < available.length) {
600
915
  const p = available[sel];
601
- if (!p) return;
602
916
  const set = new Set(dlp.patterns);
603
917
  if (set.has(p)) set.delete(p);
604
918
  else set.add(p);
605
- void persist({ ...dlp, patterns: [...set] });
606
- } else if (input === "m") {
607
- const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
608
- void persist({ ...dlp, mode: MODES2[idx] });
609
- }
919
+ mutate({ ...dlp, patterns: [...set] });
920
+ } else if (input === "d" && sel >= available.length) {
921
+ const ci = sel - available.length;
922
+ mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
923
+ setSel((n) => Math.max(0, n - 1));
924
+ } else if (input === "s") void save();
925
+ else if (input === "x") discard();
610
926
  },
611
- { isActive: focused }
927
+ { isActive: focused && !adding }
612
928
  );
613
929
  const enabled = new Set(dlp?.patterns ?? []);
614
- return /* @__PURE__ */ jsx4(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
615
- /* @__PURE__ */ jsxs4(Box4, { children: [
616
- /* @__PURE__ */ jsx4(Text4, { children: "mode: " }),
617
- /* @__PURE__ */ jsx4(Text4, { color: modeColor(dlp.mode), children: dlp.mode }),
618
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? " \u2191\u2193 move \xB7 space toggle \xB7 m mode" : " press \u2192 to edit" })
930
+ return /* @__PURE__ */ jsx5(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
931
+ /* @__PURE__ */ jsxs5(Box5, { children: [
932
+ /* @__PURE__ */ jsx5(Text5, { children: "mode: " }),
933
+ /* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
934
+ dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
619
935
  ] }),
620
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
936
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 s save" : "press \u2192 to edit" }),
937
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
621
938
  const on = enabled.has(p);
622
- return /* @__PURE__ */ jsxs4(Text4, { color: focused && i === sel ? theme.accentBright : void 0, bold: focused && i === sel, children: [
623
- (focused && i === sel ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
624
- /* @__PURE__ */ jsx4(Text4, { color: on ? theme.ok : theme.dim, children: p })
939
+ const active2 = focused && sel === i;
940
+ return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
941
+ (active2 ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
942
+ /* @__PURE__ */ jsx5(Text5, { color: on ? theme.ok : theme.dim, children: p })
625
943
  ] }, p);
626
944
  }) }),
627
- dlp.custom.length ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
628
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Custom:" }),
629
- dlp.custom.map((c2) => /* @__PURE__ */ jsxs4(Text4, { children: [
630
- " ",
631
- /* @__PURE__ */ jsx4(Text4, { color: theme.accent, children: c2.name }),
632
- " ",
633
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: c2.re })
634
- ] }, c2.name))
945
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
946
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Custom patterns" }),
947
+ dlp.custom.length === 0 ? /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " (none \u2014 press a to add)" }) : null,
948
+ dlp.custom.map((c2, i) => {
949
+ const active2 = focused && sel === available.length + i;
950
+ return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
951
+ active2 ? "\u25B8 " : " ",
952
+ /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: c2.name }),
953
+ " ",
954
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: c2.re })
955
+ ] }, c2.name + i);
956
+ })
957
+ ] }),
958
+ adding ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
959
+ /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: adding === "name" ? "New pattern name: " : `Regex for "${newName}": ` }),
960
+ adding === "name" ? /* @__PURE__ */ jsx5(
961
+ TextInput2,
962
+ {
963
+ value: newName,
964
+ onChange: setNewName,
965
+ onSubmit: (v) => {
966
+ if (!v.trim()) return setAdding(null);
967
+ setNewName(v.trim());
968
+ setAdding("re");
969
+ }
970
+ }
971
+ ) : /* @__PURE__ */ jsx5(
972
+ TextInput2,
973
+ {
974
+ value: newRe,
975
+ onChange: setNewRe,
976
+ onSubmit: (v) => {
977
+ if (v.trim() && dlp) mutate({ ...dlp, custom: [...dlp.custom, { name: newName, re: v.trim() }] });
978
+ setAdding(null);
979
+ }
980
+ }
981
+ )
635
982
  ] }) : null,
636
- status ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
983
+ status ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
637
984
  ] }) : null });
638
985
  }
639
986
 
640
987
  // src/tui/panels/Stats.tsx
641
- import { Box as Box5, Text as Text5 } from "ink";
642
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
988
+ import { Box as Box6, Text as Text6 } from "ink";
989
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
643
990
  function StatsPanel({ active: active2 }) {
644
991
  const stats = useLoader(() => api.stats.get());
645
992
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
@@ -649,50 +996,50 @@ function StatsPanel({ active: active2 }) {
649
996
  }, 5e3, active2);
650
997
  const s = stats.data;
651
998
  const points = ts.data?.timeseries ?? [];
652
- return /* @__PURE__ */ jsx5(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
653
- /* @__PURE__ */ jsxs5(Box5, { children: [
654
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
999
+ return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1000
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1001
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
655
1002
  s.total_calls,
656
1003
  " "
657
1004
  ] }),
658
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "calls " }),
659
- /* @__PURE__ */ jsxs5(Text5, { color: theme.ok, children: [
1005
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "calls " }),
1006
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.ok, children: [
660
1007
  s.allowed,
661
1008
  " allowed"
662
1009
  ] }),
663
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " " }),
664
- /* @__PURE__ */ jsxs5(Text5, { color: theme.bad, children: [
1010
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " " }),
1011
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.bad, children: [
665
1012
  s.denied,
666
1013
  " denied"
667
1014
  ] })
668
1015
  ] }),
669
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
1016
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
670
1017
  s.active_policies,
671
1018
  " policies \xB7 ",
672
1019
  s.registered_tools,
673
1020
  " tools"
674
1021
  ] }),
675
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
676
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
1022
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1023
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
677
1024
  "Last 24h ",
678
1025
  ts.data ? `(per ${ts.data.granularity})` : ""
679
1026
  ] }),
680
- /* @__PURE__ */ jsxs5(Box5, { children: [
681
- /* @__PURE__ */ jsx5(Text5, { children: "total " }),
682
- /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
1027
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1028
+ /* @__PURE__ */ jsx6(Text6, { children: "total " }),
1029
+ /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
683
1030
  ] }),
684
- /* @__PURE__ */ jsxs5(Box5, { children: [
685
- /* @__PURE__ */ jsx5(Text5, { children: "allowed " }),
686
- /* @__PURE__ */ jsx5(Text5, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
1031
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1032
+ /* @__PURE__ */ jsx6(Text6, { children: "allowed " }),
1033
+ /* @__PURE__ */ jsx6(Text6, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
687
1034
  ] }),
688
- /* @__PURE__ */ jsxs5(Box5, { children: [
689
- /* @__PURE__ */ jsx5(Text5, { children: "denied " }),
690
- /* @__PURE__ */ jsx5(Text5, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
1035
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1036
+ /* @__PURE__ */ jsx6(Text6, { children: "denied " }),
1037
+ /* @__PURE__ */ jsx6(Text6, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
691
1038
  ] })
692
1039
  ] }),
693
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
694
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Recent" }),
695
- /* @__PURE__ */ jsx5(
1040
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1041
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "Recent" }),
1042
+ /* @__PURE__ */ jsx6(
696
1043
  Table,
697
1044
  {
698
1045
  columns: [
@@ -714,111 +1061,183 @@ function StatsPanel({ active: active2 }) {
714
1061
  }
715
1062
 
716
1063
  // src/tui/panels/Audit.tsx
717
- import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
1064
+ import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1065
+ import TextInput3 from "ink-text-input";
718
1066
  import { useState as useState5 } from "react";
719
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
720
- var FILTERS = [void 0, "DENY", "ALLOW"];
1067
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1068
+ var DECISIONS = [void 0, "DENY", "ALLOW"];
1069
+ var SIGNALS = [void 0, "dlp", "ratelimit"];
721
1070
  function AuditPanel({ active: active2, focused }) {
722
- const [fi, setFi] = useState5(0);
723
- const filter = FILTERS[fi];
724
- const audit = useLoader(() => api.audit.list({ filter, limit: 14 }), [fi]);
725
- usePoll(audit.reload, 5e3, active2);
1071
+ const [di, setDi] = useState5(0);
1072
+ const [gi, setGi] = useState5(0);
1073
+ const [tool, setTool] = useState5("");
1074
+ const [agent, setAgent] = useState5("");
1075
+ const [search, setSearch] = useState5("");
1076
+ const [sel, setSel] = useState5(0);
1077
+ const [view, setView] = useState5("list");
1078
+ const [editing, setEditing] = useState5(null);
1079
+ const query = {
1080
+ filter: DECISIONS[di],
1081
+ signal: SIGNALS[gi],
1082
+ tool: tool || void 0,
1083
+ agent_name: agent || void 0,
1084
+ search: search || void 0,
1085
+ limit: 100
1086
+ };
1087
+ const auditQ = useLoader(() => api.audit.list(query), [di, gi, tool, agent, search]);
1088
+ usePoll(auditQ.reload, 6e3, active2 && view === "list" && !editing);
1089
+ const entries = auditQ.data?.entries ?? [];
1090
+ const current = entries[Math.min(sel, Math.max(0, entries.length - 1))];
1091
+ const sessionQ = useLoader(
1092
+ () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
1093
+ [view, current?.session_id]
1094
+ );
726
1095
  useInput4(
727
- (input) => {
728
- if (input === "f") setFi((n) => (n + 1) % FILTERS.length);
1096
+ (input, key) => {
1097
+ if (view === "detail") {
1098
+ if (key.leftArrow || key.escape) setView("list");
1099
+ return;
1100
+ }
1101
+ if (key.upArrow) setSel((n) => Math.max(0, n - 1));
1102
+ else if (key.downArrow) setSel((n) => Math.min(entries.length - 1, n + 1));
1103
+ else if (key.return || key.rightArrow) {
1104
+ if (current) setView("detail");
1105
+ } else if (input === "f") setDi((n) => (n + 1) % DECISIONS.length);
1106
+ else if (input === "g") setGi((n) => (n + 1) % SIGNALS.length);
1107
+ else if (input === "t") setEditing("tool");
1108
+ else if (input === "n") setEditing("agent");
1109
+ else if (input === "/") setEditing("search");
1110
+ else if (input === "c") {
1111
+ setDi(0);
1112
+ setGi(0);
1113
+ setTool("");
1114
+ setAgent("");
1115
+ setSearch("");
1116
+ }
729
1117
  },
730
- { isActive: focused }
1118
+ { isActive: focused && !editing }
731
1119
  );
732
- const entries = audit.data?.entries ?? [];
733
- return /* @__PURE__ */ jsx6(DataView, { loading: audit.loading && !audit.data, error: audit.error, empty: !!audit.data && entries.length === 0, emptyText: "No audit entries.", children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
734
- /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
735
- audit.data?.total ?? 0,
736
- " entries \xB7 filter: ",
737
- /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: filter ?? "all" }),
738
- " (press f)"
1120
+ if (view === "detail" && current) {
1121
+ const sessionCalls = sessionQ.data?.entries ?? [];
1122
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1123
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1124
+ /* @__PURE__ */ jsx7(Text7, { color: decisionColor(current.decision), bold: true, children: current.decision }),
1125
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: " " + current.tool_name }),
1126
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " + current.permission + " \xB7 " + current.trust_level })
1127
+ ] }),
1128
+ /* @__PURE__ */ jsx7(Detail, { label: "reason", value: current.reason ?? "\u2014" }),
1129
+ /* @__PURE__ */ jsx7(Detail, { label: "rule", value: current.matched_rule_id ?? "\u2014" }),
1130
+ /* @__PURE__ */ jsx7(Detail, { label: "agent", value: current.agent_name ?? "\u2014" }),
1131
+ /* @__PURE__ */ jsx7(Detail, { label: "session", value: current.session_id ?? "\u2014" }),
1132
+ /* @__PURE__ */ jsx7(Detail, { label: "dlp", value: current.dlp_matches?.length ? current.dlp_matches.join(", ") : "none", color: current.dlp_matches?.length ? theme.bad : void 0 }),
1133
+ /* @__PURE__ */ jsx7(Detail, { label: "when", value: new Date(current.created_at).toLocaleString() }),
1134
+ current.arguments_summary ? /* @__PURE__ */ jsx7(Detail, { label: "args", value: truncate(JSON.stringify(current.arguments_summary), 60) }) : null,
1135
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
1136
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1137
+ "Session ",
1138
+ current.session_id ? `\xB7 ${sessionCalls.length} calls` : "(no session id)"
1139
+ ] }),
1140
+ current.session_id ? /* @__PURE__ */ jsx7(
1141
+ Table,
1142
+ {
1143
+ columns: [
1144
+ { header: "DECISION", width: 9 },
1145
+ { header: "TOOL", width: 20 },
1146
+ { header: "REASON", width: 26 },
1147
+ { header: "WHEN", width: 6 }
1148
+ ],
1149
+ rows: sessionCalls.slice(0, 12).map((e) => [
1150
+ { value: e.decision, color: decisionColor(e.decision) },
1151
+ { value: truncate(e.tool_name, 20), color: theme.accent },
1152
+ { value: truncate(e.reason ?? "\u2014", 26) },
1153
+ { value: ago(e.created_at), dim: true }
1154
+ ])
1155
+ }
1156
+ ) : null
1157
+ ] }),
1158
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2190 back to list" })
1159
+ ] });
1160
+ }
1161
+ const chip = (label, val, on) => /* @__PURE__ */ jsxs7(Text7, { children: [
1162
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1163
+ label,
1164
+ ":"
1165
+ ] }),
1166
+ /* @__PURE__ */ jsx7(Text7, { color: on ? theme.accentBright : theme.dim, children: val }),
1167
+ /* @__PURE__ */ jsx7(Text7, { children: " " })
1168
+ ] });
1169
+ return /* @__PURE__ */ jsx7(DataView, { loading: auditQ.loading && !auditQ.data, error: auditQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1170
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1171
+ chip("dec", DECISIONS[di] ?? "all", di !== 0),
1172
+ chip("sig", SIGNALS[gi] ?? "all", gi !== 0),
1173
+ chip("tool", tool || "\xB7", !!tool),
1174
+ chip("agent", agent || "\xB7", !!agent),
1175
+ chip("search", search || "\xB7", !!search)
739
1176
  ] }),
740
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(
1177
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 enter detail \xB7 f dec \xB7 g sig \xB7 t tool \xB7 n agent \xB7 / search \xB7 c clear" : "press \u2192 to browse" }),
1178
+ editing ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1179
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
1180
+ editing,
1181
+ ": "
1182
+ ] }),
1183
+ /* @__PURE__ */ jsx7(
1184
+ TextInput3,
1185
+ {
1186
+ value: editing === "tool" ? tool : editing === "agent" ? agent : search,
1187
+ onChange: editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch,
1188
+ onSubmit: () => setEditing(null)
1189
+ }
1190
+ )
1191
+ ] }) : null,
1192
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1193
+ auditQ.data?.total ?? 0,
1194
+ " matched \xB7 showing ",
1195
+ Math.min(entries.length, 12)
1196
+ ] }) }),
1197
+ /* @__PURE__ */ jsx7(
741
1198
  Table,
742
1199
  {
743
1200
  columns: [
1201
+ { header: "", width: 2 },
744
1202
  { header: "DECISION", width: 9 },
745
1203
  { header: "TOOL", width: 18 },
746
1204
  { header: "AGENT", width: 14 },
747
- { header: "REASON", width: 26 },
1205
+ { header: "REASON", width: 24 },
748
1206
  { header: "DLP", width: 4 },
749
1207
  { header: "WHEN", width: 6 }
750
1208
  ],
751
- rows: entries.map((e) => [
752
- { value: e.decision, color: decisionColor(e.decision) },
753
- { value: truncate(e.tool_name, 18), color: theme.accent },
754
- { value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
755
- { value: truncate(e.reason ?? "\u2014", 26) },
756
- { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
757
- { value: ago(e.created_at), dim: true }
758
- ])
1209
+ rows: entries.slice(0, 12).map((e, i) => rowFor(e, i === sel && focused))
759
1210
  }
760
- ) })
1211
+ )
761
1212
  ] }) });
762
1213
  }
763
-
764
- // src/tui/panels/Agents.tsx
765
- import { Box as Box7, Text as Text7 } from "ink";
766
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
767
- function AgentsPanel({ active: active2 }) {
768
- const agents = useLoader(() => api.agents.live({ limit: 14 }));
769
- usePoll(agents.reload, 4e3, active2);
770
- const data = agents.data;
771
- const rows = data?.agents ?? [];
772
- return /* @__PURE__ */ jsx7(DataView, { loading: agents.loading && !data, error: agents.error, empty: !!data && rows.length === 0, emptyText: "No agent sessions.", children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
773
- data ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
774
- /* @__PURE__ */ jsxs7(Text7, { color: theme.ok, children: [
775
- data.counts.active,
776
- " active"
777
- ] }),
778
- " \xB7 ",
779
- /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
780
- data.counts.idle,
781
- " idle"
782
- ] }),
783
- " \xB7 ",
784
- data.counts.deactivated,
785
- " off"
786
- ] }) : null,
787
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
788
- Table,
789
- {
790
- columns: [
791
- { header: "STATUS", width: 7 },
792
- { header: "AGENT", width: 20 },
793
- { header: "CALLS", width: 6 },
794
- { header: "DENY", width: 5 },
795
- { header: "DLP", width: 4 },
796
- { header: "TRUST", width: 6 },
797
- { header: "CHARACTER", width: 20 }
798
- ],
799
- rows: rows.map((a) => [
800
- { value: a.status, color: modeColor(a.status) },
801
- { value: truncate(a.agent_name ?? a.agent_id ?? a.session_id, 20), color: theme.accent },
802
- { value: String(a.total_calls) },
803
- { value: String(a.denied_calls), color: a.denied_calls ? theme.bad : void 0, dim: !a.denied_calls },
804
- { value: String(a.dlp_events), color: a.dlp_events ? theme.bad : void 0, dim: !a.dlp_events },
805
- { value: `${a.trust_score}` },
806
- { value: truncate(a.character || "\u2014", 20), dim: true }
807
- ])
808
- }
809
- ) })
810
- ] }) });
1214
+ function rowFor(e, active2) {
1215
+ return [
1216
+ { value: active2 ? "\u25B8" : "", color: theme.accentBright },
1217
+ { value: e.decision, color: decisionColor(e.decision) },
1218
+ { value: truncate(e.tool_name, 18), color: theme.accent },
1219
+ { value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
1220
+ { value: truncate(e.reason ?? "\u2014", 24) },
1221
+ { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
1222
+ { value: ago(e.created_at), dim: true }
1223
+ ];
1224
+ }
1225
+ function Detail({ label, value, color }) {
1226
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
1227
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: label.padEnd(9) }),
1228
+ /* @__PURE__ */ jsx7(Text7, { color, children: value })
1229
+ ] });
811
1230
  }
812
1231
 
813
1232
  // src/tui/App.tsx
814
1233
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
815
1234
  var SECTIONS = [
1235
+ { label: "Live", Panel: LivePanel },
816
1236
  { label: "Policies", Panel: PoliciesPanel },
817
1237
  { label: "Rate Limit", Panel: RateLimitPanel },
818
1238
  { label: "DLP", Panel: DlpPanel },
819
1239
  { label: "Stats", Panel: StatsPanel },
820
- { label: "Audit", Panel: AuditPanel },
821
- { label: "Agents", Panel: AgentsPanel }
1240
+ { label: "Audit", Panel: AuditPanel }
822
1241
  ];
823
1242
  var BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8FA"];
824
1243
  function Banner() {