@solongate/proxy 0.67.0 → 0.69.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/index.js +233 -123
- package/dist/tui/index.js +203 -93
- package/package.json +1 -1
package/dist/tui/index.js
CHANGED
|
@@ -8,7 +8,7 @@ var __export = (target, all) => {
|
|
|
8
8
|
import { render } from "ink";
|
|
9
9
|
|
|
10
10
|
// src/tui/App.tsx
|
|
11
|
-
import { Box as Box8, Text as Text8, useApp, useInput as
|
|
11
|
+
import { Box as Box8, Text as Text8, useApp, useInput as useInput6 } from "ink";
|
|
12
12
|
import { useState as useState7 } from "react";
|
|
13
13
|
|
|
14
14
|
// src/cli-utils.ts
|
|
@@ -125,10 +125,10 @@ function KeyHints({ hints }) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// src/tui/panels/Live.tsx
|
|
128
|
-
import { Box as Box2, Text as Text2 } from "ink";
|
|
129
|
-
import { closeSync, openSync, readSync, statSync } from "fs";
|
|
128
|
+
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
129
|
+
import { closeSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "fs";
|
|
130
130
|
import { homedir as homedir2 } from "os";
|
|
131
|
-
import { join as join2 } from "path";
|
|
131
|
+
import { dirname, join as join2 } from "path";
|
|
132
132
|
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
133
133
|
|
|
134
134
|
// src/api-client/client.ts
|
|
@@ -436,8 +436,11 @@ function usePoll(reload, intervalMs, enabled = true) {
|
|
|
436
436
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
437
437
|
var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
438
438
|
var BG = "#12234f";
|
|
439
|
+
var DIM_FLOOR = "#233457";
|
|
439
440
|
var LOCAL_LOG = join2(homedir2(), ".solongate", "local-logs", "solongate-audit.jsonl");
|
|
440
441
|
var RING = join2(process.cwd(), ".solongate", ".eval-ring.jsonl");
|
|
442
|
+
var DUMP = join2(homedir2(), ".solongate", "live-stream.log");
|
|
443
|
+
var STREAM_KEEP = 100;
|
|
441
444
|
var hhmmss = (ts) => {
|
|
442
445
|
const d = new Date(ts);
|
|
443
446
|
return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
|
|
@@ -447,7 +450,7 @@ var fmtUp = (ms) => {
|
|
|
447
450
|
const p = (n) => String(n).padStart(2, "0");
|
|
448
451
|
return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
|
|
449
452
|
};
|
|
450
|
-
function tailLines(file, maxBytes =
|
|
453
|
+
function tailLines(file, maxBytes = 131072) {
|
|
451
454
|
try {
|
|
452
455
|
const size = statSync(file).size;
|
|
453
456
|
const start = Math.max(0, size - maxBytes);
|
|
@@ -462,6 +465,9 @@ function tailLines(file, maxBytes = 65536) {
|
|
|
462
465
|
return [];
|
|
463
466
|
}
|
|
464
467
|
}
|
|
468
|
+
function oscCopy(text) {
|
|
469
|
+
process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
|
|
470
|
+
}
|
|
465
471
|
function cloudItem(e) {
|
|
466
472
|
return {
|
|
467
473
|
id: "c:" + e.id,
|
|
@@ -471,7 +477,11 @@ function cloudItem(e) {
|
|
|
471
477
|
permission: (e.permission ?? "").slice(0, 4),
|
|
472
478
|
detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
|
|
473
479
|
dlp: !!e.dlp_matches?.length,
|
|
474
|
-
source: "cloud"
|
|
480
|
+
source: "cloud",
|
|
481
|
+
session: e.session_id ?? void 0,
|
|
482
|
+
agent: e.agent_name ?? void 0,
|
|
483
|
+
evalMs: e.evaluation_time_ms ?? void 0,
|
|
484
|
+
rule: e.matched_rule_id ?? void 0
|
|
475
485
|
};
|
|
476
486
|
}
|
|
477
487
|
function ColumnChart({ series, hot, height, width, color, hotColor = theme.bad }) {
|
|
@@ -490,7 +500,7 @@ function ColumnChart({ series, hot, height, width, color, hotColor = theme.bad }
|
|
|
490
500
|
else if (frac >= (r - 0.5) / height) ch = "\u2584";
|
|
491
501
|
else if (r === 1) ch = "\u2581";
|
|
492
502
|
else ch = " ";
|
|
493
|
-
const c2 = r === 1 && v === 0 ?
|
|
503
|
+
const c2 = r === 1 && v === 0 ? DIM_FLOOR : hotPad?.[i] ? hotColor : color;
|
|
494
504
|
const last = segs[segs.length - 1];
|
|
495
505
|
if (last && last.color === c2) last.text += ch;
|
|
496
506
|
else segs.push({ text: ch, color: c2 });
|
|
@@ -506,7 +516,7 @@ function HBar({ label, value, max, width, color }) {
|
|
|
506
516
|
return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
507
517
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label.padEnd(10) }),
|
|
508
518
|
/* @__PURE__ */ jsx2(Text2, { color, children: "\u2588".repeat(Math.min(width, filled)) }),
|
|
509
|
-
/* @__PURE__ */ jsx2(Text2, { color:
|
|
519
|
+
/* @__PURE__ */ jsx2(Text2, { color: DIM_FLOOR, children: "\u2591".repeat(Math.max(0, width - filled)) }),
|
|
510
520
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " " + value })
|
|
511
521
|
] });
|
|
512
522
|
}
|
|
@@ -522,6 +532,7 @@ function PaneTitle({ label, extra, width }) {
|
|
|
522
532
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width - used)) })
|
|
523
533
|
] });
|
|
524
534
|
}
|
|
535
|
+
var FILTERS = ["all", "local", "cloud"];
|
|
525
536
|
function LivePanel({ active: active2 }) {
|
|
526
537
|
const [s, setS] = useState2(null);
|
|
527
538
|
const [tsData, setTsData] = useState2(null);
|
|
@@ -531,6 +542,9 @@ function LivePanel({ active: active2 }) {
|
|
|
531
542
|
const [localOn, setLocalOn] = useState2(null);
|
|
532
543
|
const [ring, setRing] = useState2(null);
|
|
533
544
|
const [log, setLog] = useState2([]);
|
|
545
|
+
const [filter, setFilter] = useState2("all");
|
|
546
|
+
const [scroll, setScroll] = useState2(0);
|
|
547
|
+
const [toast, setToast] = useState2(null);
|
|
534
548
|
const seenRef = useRef2(/* @__PURE__ */ new Set());
|
|
535
549
|
const lastLocalTs = useRef2(0);
|
|
536
550
|
const pausedUntil = useRef2(0);
|
|
@@ -568,10 +582,14 @@ function LivePanel({ active: active2 }) {
|
|
|
568
582
|
at,
|
|
569
583
|
tool: j.tool ?? "?",
|
|
570
584
|
decision: j.decision ?? "ALLOW",
|
|
571
|
-
permission: "",
|
|
585
|
+
permission: (j.permission ?? "").slice(0, 4),
|
|
572
586
|
detail: (j.arguments ? JSON.stringify(j.arguments) : j.reason ?? "").replace(/\s+/g, " "),
|
|
573
587
|
dlp: !!j.dlp,
|
|
574
|
-
source: "local"
|
|
588
|
+
source: "local",
|
|
589
|
+
session: j.session_id,
|
|
590
|
+
agent: j.agent_name,
|
|
591
|
+
evalMs: j.evaluation_time_ms,
|
|
592
|
+
rule: j.matched_rule_id
|
|
575
593
|
});
|
|
576
594
|
} catch {
|
|
577
595
|
}
|
|
@@ -580,9 +598,7 @@ function LivePanel({ active: active2 }) {
|
|
|
580
598
|
lastLocalTs.current = fresh[fresh.length - 1].at;
|
|
581
599
|
setLocalBuf((prev) => [...prev, ...fresh].slice(-400));
|
|
582
600
|
const denies = fresh.filter((f) => f.decision !== "ALLOW").length;
|
|
583
|
-
if (
|
|
584
|
-
pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
|
|
585
|
-
}
|
|
601
|
+
if (fresh.length < 10) pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
|
|
586
602
|
}
|
|
587
603
|
const ringLines = tailLines(RING, 8192).slice(-30);
|
|
588
604
|
if (ringLines.length) {
|
|
@@ -619,9 +635,7 @@ function LivePanel({ active: active2 }) {
|
|
|
619
635
|
const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
|
|
620
636
|
const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
|
|
621
637
|
for (const e of fresh) seenRef.current.add(e.id);
|
|
622
|
-
if (fresh.length)
|
|
623
|
-
setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
|
|
624
|
-
}
|
|
638
|
+
if (fresh.length) setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
|
|
625
639
|
const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
|
|
626
640
|
if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
|
|
627
641
|
else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
|
|
@@ -661,16 +675,20 @@ function LivePanel({ active: active2 }) {
|
|
|
661
675
|
}, [active2, pollFeed, pollMedium, pollSlow]);
|
|
662
676
|
const sessions = useLoader(() => api.agents.live({ limit: 30, includeDeactivated: true }));
|
|
663
677
|
const insights = useLoader(() => api.stats.securityInsights(7));
|
|
678
|
+
const guard = useLoader(() => api.settings.getGuardStatus());
|
|
664
679
|
usePoll(() => {
|
|
665
680
|
if (!paused()) sessions.reload();
|
|
666
681
|
}, 1e4, active2);
|
|
667
682
|
usePoll(() => {
|
|
668
683
|
if (!paused()) insights.reload();
|
|
669
684
|
}, 3e4, active2);
|
|
685
|
+
usePoll(() => {
|
|
686
|
+
if (!paused()) guard.reload();
|
|
687
|
+
}, 6e4, active2);
|
|
670
688
|
const [tick, setTick] = useState2(0);
|
|
671
689
|
useEffect2(() => {
|
|
672
690
|
if (!active2) return;
|
|
673
|
-
const t = setInterval(() => setTick((n) => n + 1),
|
|
691
|
+
const t = setInterval(() => setTick((n) => n + 1), 500);
|
|
674
692
|
return () => clearInterval(t);
|
|
675
693
|
}, [active2]);
|
|
676
694
|
const startRef = useRef2(Date.now());
|
|
@@ -678,19 +696,34 @@ function LivePanel({ active: active2 }) {
|
|
|
678
696
|
const rows = process.stdout.rows ?? 30;
|
|
679
697
|
const ins = insights.data ?? {};
|
|
680
698
|
const spin = SPIN[tick % SPIN.length];
|
|
699
|
+
const nowMs = Date.now();
|
|
700
|
+
const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
|
|
701
|
+
const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => e.source === filter);
|
|
702
|
+
const visible = filtered.slice(-STREAM_KEEP);
|
|
681
703
|
const points = tsData?.timeseries ?? [];
|
|
682
|
-
const
|
|
683
|
-
const
|
|
704
|
+
const minuteCounts = new Array(60).fill(0);
|
|
705
|
+
const minuteHot = new Array(60).fill(false);
|
|
706
|
+
for (const e of mergedAll) {
|
|
707
|
+
const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
|
|
708
|
+
if (idx >= 0 && idx < 60) {
|
|
709
|
+
minuteCounts[idx]++;
|
|
710
|
+
if (e.decision !== "ALLOW") minuteHot[idx] = true;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const localTraffic = localOn === true && minuteCounts.some((c2) => c2 > 0);
|
|
714
|
+
const traffic = localTraffic ? minuteCounts : points.map((p) => p.total);
|
|
715
|
+
const trafficHot = localTraffic ? minuteHot : points.map((p) => p.denied > 0);
|
|
716
|
+
const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
|
|
684
717
|
const rl = ins.layers?.rateLimit;
|
|
685
718
|
const dl = ins.layers?.dlp;
|
|
686
|
-
const
|
|
719
|
+
const gh = ins.layers?.ghost;
|
|
720
|
+
const minuteNow = minuteCounts[59] ?? 0;
|
|
687
721
|
const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
|
|
688
722
|
const maxDlpBar = dlpBars[0]?.count ?? 1;
|
|
689
|
-
const
|
|
690
|
-
const denials = merged.filter((e) => e.decision !== "ALLOW");
|
|
723
|
+
const denials = mergedAll.filter((e) => e.decision !== "ALLOW");
|
|
691
724
|
const lastDeny = denials[denials.length - 1];
|
|
692
725
|
const toolCounts = /* @__PURE__ */ new Map();
|
|
693
|
-
for (const e of
|
|
726
|
+
for (const e of mergedAll) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
|
|
694
727
|
const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
695
728
|
const latNow = lat[lat.length - 1] ?? 0;
|
|
696
729
|
const sortedLat = [...lat].sort((a, b) => a - b);
|
|
@@ -699,20 +732,65 @@ function LivePanel({ active: active2 }) {
|
|
|
699
732
|
const backingOff = Date.now() < pausedUntil.current;
|
|
700
733
|
const sess = (sessions.data?.agents ?? []).slice().sort((a, b) => Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
|
|
701
734
|
const sessCounts = sessions.data?.counts;
|
|
735
|
+
const localSess = /* @__PURE__ */ new Map();
|
|
736
|
+
for (const e of localBuf) {
|
|
737
|
+
if (!e.session) continue;
|
|
738
|
+
const cur = localSess.get(e.session) ?? { agent: e.agent ?? "local agent", calls: 0, denies: 0, lastAt: 0 };
|
|
739
|
+
cur.calls++;
|
|
740
|
+
if (e.decision !== "ALLOW") cur.denies++;
|
|
741
|
+
cur.lastAt = Math.max(cur.lastAt, e.at);
|
|
742
|
+
if (e.agent) cur.agent = e.agent;
|
|
743
|
+
localSess.set(e.session, cur);
|
|
744
|
+
}
|
|
745
|
+
const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
|
|
702
746
|
const chartH = rows >= 36 ? 6 : 4;
|
|
703
|
-
const colH = rows >= 32 ?
|
|
704
|
-
const streamRows = Math.max(4, rows -
|
|
705
|
-
const tail = merged.slice(-streamRows);
|
|
747
|
+
const colH = rows >= 32 ? 7 : 5;
|
|
748
|
+
const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
|
|
706
749
|
const innerW = cols - 2;
|
|
707
750
|
const leftW = Math.floor(innerW * 0.55);
|
|
708
751
|
const rightW = innerW - leftW - 2;
|
|
709
752
|
const colW = Math.max(20, Math.floor((innerW - 4) / 3));
|
|
753
|
+
const maxScroll = Math.max(0, visible.length - streamRows);
|
|
754
|
+
const clampedScroll = Math.min(scroll, maxScroll);
|
|
755
|
+
const start = Math.max(0, visible.length - streamRows - clampedScroll);
|
|
756
|
+
const windowed = visible.slice(start, start + streamRows);
|
|
757
|
+
useInput(
|
|
758
|
+
(input, key) => {
|
|
759
|
+
if (input === "f") {
|
|
760
|
+
setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
|
|
761
|
+
setScroll(0);
|
|
762
|
+
} else if (key.upArrow) setScroll((n) => Math.min(maxScroll, n + 1));
|
|
763
|
+
else if (key.downArrow) setScroll((n) => Math.max(0, n - 1));
|
|
764
|
+
else if (key.pageUp) setScroll((n) => Math.min(maxScroll, n + streamRows));
|
|
765
|
+
else if (key.pageDown) setScroll((n) => Math.max(0, n - streamRows));
|
|
766
|
+
else if (input === "c") {
|
|
767
|
+
const dump = visible.map(
|
|
768
|
+
(e) => `[${new Date(e.at).toISOString()}] ${e.source.toUpperCase().padEnd(5)} ${e.decision.padEnd(6)} ${e.tool.padEnd(16)} ${e.permission.padEnd(5)} ${e.evalMs != null ? String(e.evalMs).padStart(5) + "ms " : " "}${(e.agent ?? "-").padEnd(14)} ${e.session ? e.session.slice(0, 8) : "- "} ${e.detail}`
|
|
769
|
+
).join("\n");
|
|
770
|
+
try {
|
|
771
|
+
mkdirSync(dirname(DUMP), { recursive: true });
|
|
772
|
+
writeFileSync(DUMP, dump + "\n");
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
oscCopy(dump);
|
|
776
|
+
setToast({ msg: `copied ${visible.length} lines \u2192 clipboard + ${DUMP}`, until: Date.now() + 4e3 });
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
{ isActive: active2 }
|
|
780
|
+
);
|
|
710
781
|
const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
|
|
782
|
+
if (s === null && localOn === null) {
|
|
783
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
784
|
+
spin,
|
|
785
|
+
" connecting\u2026"
|
|
786
|
+
] });
|
|
787
|
+
}
|
|
711
788
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
|
|
712
789
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
713
790
|
/* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
|
|
714
|
-
/* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(
|
|
715
|
-
backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " RATE LIMITED \xB7 backing off " }) : lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.at)} ${lastDeny.tool} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " })
|
|
791
|
+
/* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
|
|
792
|
+
backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " RATE LIMITED \xB7 backing off " }) : lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.at)} ${lastDeny.tool} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " }),
|
|
793
|
+
toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
|
|
716
794
|
] }),
|
|
717
795
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
718
796
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
|
|
@@ -722,18 +800,22 @@ function LivePanel({ active: active2 }) {
|
|
|
722
800
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
|
|
723
801
|
/* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
|
|
724
802
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 local " }),
|
|
725
|
-
localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713
|
|
726
|
-
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502
|
|
803
|
+
localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713" }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "off" }),
|
|
804
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 rl " }),
|
|
727
805
|
/* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
|
|
728
806
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
|
|
729
807
|
/* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
|
|
730
|
-
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502
|
|
731
|
-
/* @__PURE__ */ jsx2(Text2, { color:
|
|
808
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ghost " }),
|
|
809
|
+
/* @__PURE__ */ jsx2(Text2, { color: gh?.mode === "on" ? theme.ok : theme.dim, children: gh?.mode ?? "?" }),
|
|
810
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sess " }),
|
|
811
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${localSessList.length}loc ${sessCounts?.active ?? 0}\u25B2${sessCounts?.idle ?? 0}\u25CC` }),
|
|
812
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 hooks " }),
|
|
813
|
+
/* @__PURE__ */ jsx2(Text2, { color: guard.data?.up_to_date ? theme.ok : theme.warn, children: guard.data ? `v${guard.data.installed ?? "?"}${guard.data.up_to_date ? "" : "\u2192v" + guard.data.latest} ${guard.data.device_count}dev` : "\xB7" }),
|
|
732
814
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
|
|
733
815
|
] }),
|
|
734
|
-
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
816
|
+
/* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
|
|
735
817
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
|
|
736
|
-
/* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra:
|
|
818
|
+
/* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
|
|
737
819
|
/* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
|
|
738
820
|
] }),
|
|
739
821
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: rightW, children: [
|
|
@@ -741,45 +823,61 @@ function LivePanel({ active: active2 }) {
|
|
|
741
823
|
/* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), height: chartH, width: rightW, color: "#82AAF0", hotColor: "#ffb454" })
|
|
742
824
|
] })
|
|
743
825
|
] }),
|
|
744
|
-
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
745
|
-
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
|
|
826
|
+
/* @__PURE__ */ jsxs2(Box2, { height: colH, children: [
|
|
827
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
|
|
746
828
|
/* @__PURE__ */ jsx2(PaneTitle, { label: "LAYERS", width: colW }),
|
|
747
829
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
748
830
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "RATELIMIT ".padEnd(10) }),
|
|
749
831
|
/* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: (rl?.mode ?? "?").padEnd(7) }),
|
|
750
|
-
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: rl?.perMinute ? `${minuteNow}/${rl.perMinute}
|
|
832
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: rl?.perMinute ? `${minuteNow}/${rl.perMinute}m ${rl.perHour || "\u2014"}h ${rl.perDay || "\u2014"}d` : "no limits set" })
|
|
751
833
|
] }),
|
|
752
|
-
rl?.perMinute ? /* @__PURE__ */ jsx2(HBar, { label: "load", value: minuteNow, max: rl.perMinute, width: Math.max(6, colW - 16), color: minuteNow > rl.perMinute ? theme.bad : theme.accent }) : null,
|
|
834
|
+
rl?.perMinute ? /* @__PURE__ */ jsx2(HBar, { label: "load/min", value: minuteNow, max: rl.perMinute, width: Math.max(6, colW - 16), color: minuteNow > rl.perMinute ? theme.bad : theme.accent }) : null,
|
|
753
835
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
754
836
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "DLP".padEnd(10) }),
|
|
755
837
|
/* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: (dl?.mode ?? "?").padEnd(7) }),
|
|
756
|
-
/* @__PURE__ */
|
|
757
|
-
(dl?.patterns ?? []).length,
|
|
758
|
-
" patterns armed"
|
|
759
|
-
] })
|
|
838
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${(dl?.patterns ?? []).length} builtin \xB7 ${(dl?.custom ?? []).length} custom` })
|
|
760
839
|
] }),
|
|
761
|
-
dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d.pattern, 10), value: d.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "hits
|
|
762
|
-
|
|
840
|
+
dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d.pattern, 10), value: d.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " no dlp hits in last 7 days" }),
|
|
841
|
+
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
763
842
|
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
|
|
764
|
-
/* @__PURE__ */
|
|
765
|
-
|
|
766
|
-
|
|
843
|
+
ring ? /* @__PURE__ */ jsxs2(Text2, { color: theme.ok, children: [
|
|
844
|
+
"local \u2713 ",
|
|
845
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `eval avg ${ring.avgMs}ms` })
|
|
846
|
+
] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no local ring in cwd" })
|
|
847
|
+
] })
|
|
767
848
|
] }),
|
|
768
|
-
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
|
|
769
|
-
/* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra:
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
849
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
|
|
850
|
+
/* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud`, width: colW }),
|
|
851
|
+
localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
|
|
852
|
+
localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
|
|
853
|
+
const freshDot = nowMs - v.lastAt < 9e4;
|
|
854
|
+
const isMe = ring?.session === id;
|
|
855
|
+
return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
856
|
+
/* @__PURE__ */ jsxs2(Text2, { color: freshDot ? theme.ok : theme.dim, children: [
|
|
857
|
+
freshDot ? "\u25CF" : "\u25CB",
|
|
858
|
+
" "
|
|
859
|
+
] }),
|
|
860
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
|
|
861
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
|
|
862
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
863
|
+
String(v.calls).padStart(4),
|
|
864
|
+
"c "
|
|
865
|
+
] }),
|
|
866
|
+
/* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
|
|
867
|
+
String(v.denies).padStart(3),
|
|
868
|
+
"d "
|
|
869
|
+
] }),
|
|
870
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
|
|
871
|
+
] }, id);
|
|
872
|
+
}),
|
|
873
|
+
sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
|
|
777
874
|
const dot = sessionDot(a.status);
|
|
778
875
|
return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
779
876
|
/* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
|
|
780
877
|
dot.ch,
|
|
781
878
|
" "
|
|
782
879
|
] }),
|
|
880
|
+
/* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
|
|
783
881
|
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(a.agent_name ?? a.session_id, 12).padEnd(13) }),
|
|
784
882
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
785
883
|
String(a.total_calls).padStart(4),
|
|
@@ -789,12 +887,12 @@ function LivePanel({ active: active2 }) {
|
|
|
789
887
|
String(a.denied_calls).padStart(3),
|
|
790
888
|
"d "
|
|
791
889
|
] }),
|
|
792
|
-
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(a.last_seen_at) })
|
|
890
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `t${a.trust_score} ` + ago(a.last_seen_at) })
|
|
793
891
|
] }, a.session_id);
|
|
794
892
|
})
|
|
795
893
|
] }),
|
|
796
|
-
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, children: [
|
|
797
|
-
/* @__PURE__ */ jsx2(PaneTitle, { label: "EVENT LOG", extra: "
|
|
894
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, overflow: "hidden", children: [
|
|
895
|
+
/* @__PURE__ */ jsx2(PaneTitle, { label: "EVENT LOG", extra: "system heartbeat", width: colW }),
|
|
798
896
|
log.slice(-(colH - 1)).map((l, i) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
799
897
|
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
800
898
|
hhmmss(l.ts),
|
|
@@ -805,39 +903,51 @@ function LivePanel({ active: active2 }) {
|
|
|
805
903
|
] }, l.ts + ":" + i))
|
|
806
904
|
] })
|
|
807
905
|
] }),
|
|
808
|
-
/* @__PURE__ */ jsx2(
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
"
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
/* @__PURE__ */
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
906
|
+
/* @__PURE__ */ jsx2(
|
|
907
|
+
PaneTitle,
|
|
908
|
+
{
|
|
909
|
+
label: "TOOL STREAM",
|
|
910
|
+
extra: `last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25B2${clampedScroll} scrolled` : " \xB7 following"} \xB7 f filter \xB7 \u2191\u2193 scroll \xB7 c copy`,
|
|
911
|
+
width: innerW
|
|
912
|
+
}
|
|
913
|
+
),
|
|
914
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
|
|
915
|
+
localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
916
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
|
|
917
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2014 enable: dashboard \u2192 Settings \u2192 Local logs \xB7 hooks write ~/.solongate/local-logs \xB7 view: `solongate logs-server`" })
|
|
918
|
+
] }) : null,
|
|
919
|
+
windowed.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
920
|
+
spin,
|
|
921
|
+
" awaiting traffic\u2026"
|
|
922
|
+
] }) : null,
|
|
923
|
+
windowed.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
924
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
|
|
925
|
+
"[",
|
|
926
|
+
hhmmss(e.at),
|
|
927
|
+
" "
|
|
928
|
+
] }),
|
|
929
|
+
/* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
|
|
930
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
|
|
931
|
+
/* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
|
|
932
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 12).padEnd(13) }),
|
|
933
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
|
|
934
|
+
/* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
|
|
935
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent ?? "-", 11).padEnd(12) }),
|
|
936
|
+
e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
|
|
937
|
+
e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate(e.rule, 14) + " " }) : null,
|
|
938
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
|
|
939
|
+
] }, e.id))
|
|
940
|
+
] }),
|
|
831
941
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
|
|
832
942
|
/* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
|
|
833
|
-
/* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"}
|
|
834
|
-
/* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " esc menu \xB7 q quit " })
|
|
943
|
+
/* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld buffered \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
|
|
944
|
+
/* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 \u2191\u2193 scroll \xB7 c copy \xB7 esc menu \xB7 q quit " })
|
|
835
945
|
] })
|
|
836
946
|
] });
|
|
837
947
|
}
|
|
838
948
|
|
|
839
949
|
// src/tui/panels/Policies.tsx
|
|
840
|
-
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
950
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
841
951
|
import TextInput from "ink-text-input";
|
|
842
952
|
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
843
953
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
@@ -928,7 +1038,7 @@ function PoliciesPanel({ focused }) {
|
|
|
928
1038
|
setDirty(false);
|
|
929
1039
|
setStatus("discarded");
|
|
930
1040
|
};
|
|
931
|
-
|
|
1041
|
+
useInput2(
|
|
932
1042
|
(input, key) => {
|
|
933
1043
|
if (view === "list") {
|
|
934
1044
|
if (key.upArrow) setPi((n) => Math.max(0, n - 1));
|
|
@@ -1072,7 +1182,7 @@ function PoliciesPanel({ focused }) {
|
|
|
1072
1182
|
}
|
|
1073
1183
|
|
|
1074
1184
|
// src/tui/panels/RateLimit.tsx
|
|
1075
|
-
import { Box as Box4, Text as Text4, useInput as
|
|
1185
|
+
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
1076
1186
|
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
1077
1187
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1078
1188
|
var MODES = ["off", "detect", "block"];
|
|
@@ -1123,7 +1233,7 @@ function RateLimitPanel({ focused }) {
|
|
|
1123
1233
|
setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
|
|
1124
1234
|
}
|
|
1125
1235
|
};
|
|
1126
|
-
|
|
1236
|
+
useInput3(
|
|
1127
1237
|
(input, key) => {
|
|
1128
1238
|
const step = key.shift ? 10 : 1;
|
|
1129
1239
|
if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
|
|
@@ -1202,7 +1312,7 @@ function FieldRow({ label, active: active2, children }) {
|
|
|
1202
1312
|
}
|
|
1203
1313
|
|
|
1204
1314
|
// src/tui/panels/Dlp.tsx
|
|
1205
|
-
import { Box as Box5, Text as Text5, useInput as
|
|
1315
|
+
import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
|
|
1206
1316
|
import TextInput2 from "ink-text-input";
|
|
1207
1317
|
import { useEffect as useEffect5, useState as useState5 } from "react";
|
|
1208
1318
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
@@ -1247,7 +1357,7 @@ function DlpPanel({ focused }) {
|
|
|
1247
1357
|
};
|
|
1248
1358
|
const customCount = dlp?.custom.length ?? 0;
|
|
1249
1359
|
const total = available.length + customCount;
|
|
1250
|
-
|
|
1360
|
+
useInput4(
|
|
1251
1361
|
(input, key) => {
|
|
1252
1362
|
if (!dlp) return;
|
|
1253
1363
|
if (key.upArrow) setSel((n) => Math.max(0, n - 1));
|
|
@@ -1409,7 +1519,7 @@ function StatsPanel({ active: active2 }) {
|
|
|
1409
1519
|
}
|
|
1410
1520
|
|
|
1411
1521
|
// src/tui/panels/Audit.tsx
|
|
1412
|
-
import { Box as Box7, Text as Text7, useInput as
|
|
1522
|
+
import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
|
|
1413
1523
|
import TextInput3 from "ink-text-input";
|
|
1414
1524
|
import { useState as useState6 } from "react";
|
|
1415
1525
|
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
@@ -1440,7 +1550,7 @@ function AuditPanel({ active: active2, focused }) {
|
|
|
1440
1550
|
() => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
|
|
1441
1551
|
[view, current?.session_id]
|
|
1442
1552
|
);
|
|
1443
|
-
|
|
1553
|
+
useInput5(
|
|
1444
1554
|
(input, key) => {
|
|
1445
1555
|
if (view === "detail") {
|
|
1446
1556
|
if (key.leftArrow || key.escape) setView("list");
|
|
@@ -1617,7 +1727,7 @@ function App() {
|
|
|
1617
1727
|
const { exit } = useApp();
|
|
1618
1728
|
const [section, setSection] = useState7(0);
|
|
1619
1729
|
const [focus, setFocus] = useState7("nav");
|
|
1620
|
-
|
|
1730
|
+
useInput6((input, key) => {
|
|
1621
1731
|
if (focus === "nav") {
|
|
1622
1732
|
if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);
|
|
1623
1733
|
else if (key.downArrow) setSection((n) => (n + 1) % SECTIONS.length);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.0",
|
|
4
4
|
"description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|