@slack/radar-mcp 1.6.0 → 1.7.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/README.md +4 -3
- package/dist/mcp/api-paths.d.ts +1 -0
- package/dist/mcp/api-paths.js +108 -0
- package/dist/mcp/index.js +51 -79
- package/dist/mcp/logs-result.d.ts +9 -0
- package/dist/mcp/logs-result.js +15 -0
- package/dist/mcp/tools.js +31 -7
- package/dist/shared/android.d.ts +2 -2
- package/dist/shared/android.js +16 -11
- package/dist/shared/screen.d.ts +10 -0
- package/dist/shared/screen.js +13 -1
- package/dist/shared/stream.d.ts +1 -1
- package/dist/shared/transport.d.ts +12 -4
- package/dist/web/public/index.html +242 -1126
- package/dist/web/public/next/app.js +210 -0
- package/dist/web/public/next/db.js +286 -0
- package/dist/web/public/next/esc.js +13 -0
- package/dist/web/public/next/feed.js +608 -0
- package/dist/web/public/next/filters.js +120 -0
- package/dist/web/public/next/hooks.js +57 -0
- package/dist/web/public/next/html.js +10 -0
- package/dist/web/public/next/inspector.js +258 -0
- package/dist/web/public/next/registry.js +65 -0
- package/dist/web/public/next/screen.js +180 -0
- package/dist/web/public/next/spec-types.js +1 -0
- package/dist/web/public/next/store.js +714 -0
- package/dist/web/public/next/widgets.js +194 -0
- package/dist/web/server.js +7 -1
- package/package.json +3 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Card widgets: rate, sparkline, graph (counter is a back-compat no-op -- see below). Each
|
|
2
|
+
// reads the live aggregates the store maintains and re-renders on the store "tick" + "feed"
|
|
3
|
+
// events. The graph SVG geometry is ported verbatim from the proven vanilla drawGraph.
|
|
4
|
+
import { html } from "./html.js";
|
|
5
|
+
import { useState } from "preact/hooks";
|
|
6
|
+
import { esc } from "./esc.js";
|
|
7
|
+
import { useStorePulse } from "./hooks.js";
|
|
8
|
+
import { store, seriesValues, unitFor, fmtNum } from "./store.js";
|
|
9
|
+
// The stats are a single THIN strip, not tall cards. Rate is an inline number, the sparkline a
|
|
10
|
+
// small inline bar chart, and the graph (when present) flex-fills the rest of the row.
|
|
11
|
+
export function Cards({ spec }) {
|
|
12
|
+
useStorePulse(store, "tick", "feed");
|
|
13
|
+
const viz = spec.viz || [];
|
|
14
|
+
// NOTE: the "counter" widget is intentionally NOT rendered. A raw count is the wrong signal
|
|
15
|
+
// for a live stream -- cumulative total disagrees with the scrollable feed, and buffer
|
|
16
|
+
// occupancy wobbles as the ring evicts. Rate is the live pulse; the feed is the data.
|
|
17
|
+
// "counter" stays a valid spec key (back-compat) but is a no-op here. Use matchedRetained
|
|
18
|
+
// for any count that must agree with the feed.
|
|
19
|
+
const hasRate = viz.includes("rate");
|
|
20
|
+
const hasSpark = viz.includes("sparkline");
|
|
21
|
+
const hasGraph = viz.includes("graph") && store.graph;
|
|
22
|
+
if (!hasRate && !hasSpark && !hasGraph)
|
|
23
|
+
return null;
|
|
24
|
+
const w = spec.rateWindowSec || 10;
|
|
25
|
+
return html `
|
|
26
|
+
<div class="statstrip">
|
|
27
|
+
${hasRate ? html `<span class="stat"><span class="statlabel">rate</span><span class="statval">${store.rate}</span><span class="statunit">/${w}s</span></span>` : null}
|
|
28
|
+
${hasSpark ? html `<${MiniSpark} />` : null}
|
|
29
|
+
${hasGraph ? html `<${CollapsibleGraph} spec=${spec} />` : null}
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
// A compact always-on mini spark that expands to the full chart as a FLOATING overlay (never
|
|
34
|
+
// steals feed height); click-outside collapses.
|
|
35
|
+
function CollapsibleGraph({ spec }) {
|
|
36
|
+
const [expanded, setExpanded] = useState(false);
|
|
37
|
+
return html `
|
|
38
|
+
<span class="cgraph">
|
|
39
|
+
<span class="cgraph-mini" title="click to expand the chart" onClick=${() => setExpanded(true)}>
|
|
40
|
+
<${MiniGraph} spec=${spec} />
|
|
41
|
+
<${GraphLegendInline} spec=${spec} />
|
|
42
|
+
<span class="cgraph-exp">⤢</span>
|
|
43
|
+
</span>
|
|
44
|
+
${expanded
|
|
45
|
+
? html `<div class="cgraph-overlay" onClick=${() => setExpanded(false)}>
|
|
46
|
+
<div class="cgraph-panel" onClick=${(e) => e.stopPropagation()}>
|
|
47
|
+
<div class="cgraph-head">
|
|
48
|
+
<span class="label">${(spec.series || []).map((s) => s.label || s.metric).join(" vs ")}</span>
|
|
49
|
+
<span class="cgraph-collapse" title="collapse" onClick=${() => setExpanded(false)}>✕</span>
|
|
50
|
+
</div>
|
|
51
|
+
<${Graph} spec=${spec} />
|
|
52
|
+
</div>
|
|
53
|
+
</div>`
|
|
54
|
+
: null}
|
|
55
|
+
</span>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
function GraphLegendInline() {
|
|
59
|
+
const G = store.graph;
|
|
60
|
+
if (!G)
|
|
61
|
+
return null;
|
|
62
|
+
return html `<span class="cgraph-vals">${G.series.map((s, i) => {
|
|
63
|
+
const vals = seriesValues(s);
|
|
64
|
+
let last = null;
|
|
65
|
+
for (const v of vals)
|
|
66
|
+
if (v != null)
|
|
67
|
+
last = v;
|
|
68
|
+
const unit = unitFor(s.def);
|
|
69
|
+
return html `<span class="cgraph-val" style=${{ color: GCOLORS[i] }}>${fmtNum(last)}${unit ? " " + unit : ""}</span>`;
|
|
70
|
+
})}</span>`;
|
|
71
|
+
}
|
|
72
|
+
// Small normalized multi-line spark; red dashed threshold line when highlight is set. SVG body
|
|
73
|
+
// is a generated string (geometry, no user text) via dangerouslySetInnerHTML -- one of the
|
|
74
|
+
// three audited sinks; the only interpolated value (the threshold) is numeric.
|
|
75
|
+
function MiniGraph({ spec }) {
|
|
76
|
+
const G = store.graph;
|
|
77
|
+
if (!G)
|
|
78
|
+
return null;
|
|
79
|
+
const W = 120, H = 28, N = G.N;
|
|
80
|
+
let out = "";
|
|
81
|
+
G.series.forEach((s, si) => {
|
|
82
|
+
const raw = seriesValues(s);
|
|
83
|
+
let last = null;
|
|
84
|
+
const vals = raw.map((v) => (v == null ? last : (last = v)));
|
|
85
|
+
const real = vals.filter((v) => v != null);
|
|
86
|
+
const mx = Math.max(1, ...real);
|
|
87
|
+
const pts = vals
|
|
88
|
+
.map((v, i) => (v == null ? null : (i / (N - 1)) * W + "," + (H - (v / mx) * (H - 2) - 1).toFixed(1)))
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(" ");
|
|
91
|
+
if (pts)
|
|
92
|
+
out += '<polyline fill="none" stroke="' + GCOLORS[si] + '" stroke-width="1.5" points="' + pts + '"/>';
|
|
93
|
+
const h = spec.highlight;
|
|
94
|
+
if (h && s.def.field && h.field === s.def.field && h.value <= mx) {
|
|
95
|
+
const ty = H - (h.value / mx) * (H - 2) - 1;
|
|
96
|
+
out += '<line x1="0" y1="' + ty.toFixed(1) + '" x2="' + W + '" y2="' + ty.toFixed(1) + '" stroke="#ff5b50" stroke-width="1" stroke-dasharray="3 2"/>';
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return html `<svg class="minigraph" viewBox=${"0 0 " + W + " " + H} preserveAspectRatio="none" dangerouslySetInnerHTML=${{ __html: out }}></svg>`;
|
|
100
|
+
}
|
|
101
|
+
function MiniSpark() {
|
|
102
|
+
const ps = store.perSec;
|
|
103
|
+
const mx = Math.max(1, ...ps);
|
|
104
|
+
// plain "per sec": Preact renders template text literally (no entity decode), and
|
|
105
|
+
// .ministat is white-space:nowrap, so a normal space does not wrap.
|
|
106
|
+
return html `<span class="ministat"><span class="statlabel">per sec</span><span class="minispark">${ps.map((n) => html `<span class="minibar" style=${{ height: 2 + (n / mx) * 20 + "px" }}></span>`)}</span></span>`;
|
|
107
|
+
}
|
|
108
|
+
const GCOLORS = ["#39d98a", "#ffd479"];
|
|
109
|
+
// Chart CHROME (frame, ticks, axis labels) reads live theme tokens so the graph follows
|
|
110
|
+
// light/dark. The data SERIES + the red threshold stay vivid in both themes by design.
|
|
111
|
+
function themeColor(varName, fallback) {
|
|
112
|
+
try {
|
|
113
|
+
const v = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
|
|
114
|
+
return v || fallback;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return fallback;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Live SVG line chart, 1-2 series, dual labeled axes + threshold reference line. Built as an
|
|
121
|
+
// innerHTML SVG string (same as vanilla drawGraph) so the geometry math is byte-for-byte the
|
|
122
|
+
// proven version; every interpolated string (units/labels) passes through the shared esc().
|
|
123
|
+
function Graph({ spec }) {
|
|
124
|
+
const G = store.graph;
|
|
125
|
+
if (!G)
|
|
126
|
+
return null;
|
|
127
|
+
const VB_W = 640, VB_H = 200, N = G.N;
|
|
128
|
+
const L = 46, R = G.series.length > 1 ? 52 : 14, T = 10, B = 26;
|
|
129
|
+
const x0 = L, x1 = VB_W - R, y0 = T, y1 = VB_H - B, pw = x1 - x0, ph = y1 - y0;
|
|
130
|
+
const cFrame = themeColor("--border", "#1e2a3a");
|
|
131
|
+
const cTick = themeColor("--border-3", "#33455c");
|
|
132
|
+
const cAxis = themeColor("--muted", "#5f7186");
|
|
133
|
+
let out = "", legend = "";
|
|
134
|
+
out += '<rect x="' + x0 + '" y="' + y0 + '" width="' + pw + '" height="' + ph + '" fill="none" stroke="' + cFrame + '"/>';
|
|
135
|
+
for (let k = 0; k <= 4; k++) {
|
|
136
|
+
const fx = x0 + (k / 4) * pw;
|
|
137
|
+
const secsAgo = Math.round((1 - k / 4) * N);
|
|
138
|
+
const lbl = secsAgo === 0 ? "now" : "-" + secsAgo + "s";
|
|
139
|
+
out += '<line x1="' + fx.toFixed(1) + '" y1="' + y1 + '" x2="' + fx.toFixed(1) + '" y2="' + (y1 + 4) + '" stroke="' + cTick + '"/>';
|
|
140
|
+
out += '<text x="' + fx.toFixed(1) + '" y="' + (y1 + 16) + '" fill="' + cAxis + '" font-size="10" text-anchor="middle">' + lbl + "</text>";
|
|
141
|
+
}
|
|
142
|
+
G.series.forEach((s, si) => {
|
|
143
|
+
const raw = seriesValues(s);
|
|
144
|
+
const scale = s.def.metric === "errorRate" ? 100 : 1;
|
|
145
|
+
let last = null;
|
|
146
|
+
const vals = raw.map((v) => {
|
|
147
|
+
const sv = v == null ? null : v * scale;
|
|
148
|
+
if (sv != null) {
|
|
149
|
+
last = sv;
|
|
150
|
+
return sv;
|
|
151
|
+
}
|
|
152
|
+
return last;
|
|
153
|
+
});
|
|
154
|
+
const real = vals.filter((v) => v != null);
|
|
155
|
+
const mx = Math.max(1, ...real);
|
|
156
|
+
const col = GCOLORS[si], unit = unitFor(s.def);
|
|
157
|
+
const axX = si === 0 ? x0 : x1, anchor = si === 0 ? "end" : "start", tx = si === 0 ? x0 - 6 : x1 + 6;
|
|
158
|
+
for (let k = 0; k <= 2; k++) {
|
|
159
|
+
const val = (mx * k) / 2;
|
|
160
|
+
const fy = y1 - (k / 2) * ph;
|
|
161
|
+
out += '<text x="' + tx + '" y="' + (fy + 3).toFixed(1) + '" fill="' + col + '" font-size="10" text-anchor="' + anchor + '">' + fmtNum(val) + "</text>";
|
|
162
|
+
}
|
|
163
|
+
out += '<text x="' + axX + '" y="' + (y0 - 1) + '" fill="' + col + '" font-size="9" text-anchor="' + (si === 0 ? "start" : "end") + '">' + esc(unit || s.def.metric) + "</text>";
|
|
164
|
+
const pts = vals
|
|
165
|
+
.map((v, i) => {
|
|
166
|
+
if (v == null)
|
|
167
|
+
return null;
|
|
168
|
+
const px = x0 + (i / (N - 1)) * pw;
|
|
169
|
+
const py = y1 - (v / mx) * ph;
|
|
170
|
+
return px.toFixed(1) + "," + py.toFixed(1);
|
|
171
|
+
})
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.join(" ");
|
|
174
|
+
if (pts)
|
|
175
|
+
out += '<polyline fill="none" stroke="' + col + '" stroke-width="2" points="' + pts + '"/>';
|
|
176
|
+
const h = spec.highlight;
|
|
177
|
+
if (h && s.def.field && h.field === s.def.field && h.value <= mx) {
|
|
178
|
+
const ty = y1 - (h.value / mx) * ph;
|
|
179
|
+
out += '<line x1="' + x0 + '" y1="' + ty.toFixed(1) + '" x2="' + x1 + '" y2="' + ty.toFixed(1) + '" stroke="#ff3b30" stroke-width="1" stroke-dasharray="4 3"/>';
|
|
180
|
+
out += '<text x="' + (x0 + 4) + '" y="' + (ty - 3).toFixed(1) + '" fill="#ff6b60" font-size="9">' + esc(h.label || ">" + h.value) + " " + esc(unit || "") + "</text>";
|
|
181
|
+
}
|
|
182
|
+
const shown = fmtNum(last) + (unit ? " " + unit : "");
|
|
183
|
+
legend += '<span style="color:' + col + '">■ ' + esc(s.def.label || s.def.metric) + ": " + shown + "</span> ";
|
|
184
|
+
});
|
|
185
|
+
const labelText = G.series.map((s) => s.def.label || s.def.metric).join(" vs ");
|
|
186
|
+
return html `
|
|
187
|
+
<div class="graphcard">
|
|
188
|
+
<div class="label">${labelText}</div>
|
|
189
|
+
<svg viewBox="0 0 640 200" dangerouslySetInnerHTML=${{ __html: out }}></svg>
|
|
190
|
+
<div class="legend" dangerouslySetInnerHTML=${{ __html: legend + ' <span style="color:var(--muted)">x: time (last ' + N + "s, right=now)</span>" }}></div>
|
|
191
|
+
</div>
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
export { seriesValues, unitFor, fmtNum };
|
package/dist/web/server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { RADAR_HOST, RADAR_PORT, RADAR_VERSION, WEB_PORT_DEFAULT } from "../shar
|
|
|
9
9
|
import { cleanupPulledDatabases, initializeDatabasePath, listDatabases, pullDatabase, pulledSize, queryDatabase, resetDbState, } from "../shared/db.js";
|
|
10
10
|
import { LogSession } from "./log-session.js";
|
|
11
11
|
import { BLANK_SPEC, coerceSpec, validateSpec } from "./spec.js";
|
|
12
|
-
import { screenCapabilities, refreshScreenCapabilities, grantScreenConsent, hasScreenConsent, detectDisplay, displayNote, captureScreenshot, startLive, addViewer, removeViewer, primeIfNeeded, startRecording, stopRecording, recordingPath, resetScreenState, registerScreenCleanup, resolveFfmpeg, ffmpegInstallHint, SCREEN_BOUNDARY, } from "../shared/screen.js";
|
|
12
|
+
import { screenCapabilities, refreshScreenCapabilities, grantScreenConsent, hasScreenConsent, detectDisplay, displayNote, captureScreenshot, startLive, addViewer, removeViewer, reapLiveIfIdle, primeIfNeeded, startRecording, stopRecording, recordingPath, resetScreenState, registerScreenCleanup, resolveFfmpeg, ffmpegInstallHint, SCREEN_BOUNDARY, } from "../shared/screen.js";
|
|
13
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const WEB_PORT = Number(process.env.SLACK_RADAR_WEB_PORT ?? WEB_PORT_DEFAULT);
|
|
15
15
|
const INDEX_PATH = path.join(__dirname, "public", "index.html");
|
|
@@ -139,6 +139,8 @@ export function createServer() {
|
|
|
139
139
|
const url = req.url ?? "";
|
|
140
140
|
const urlPath = url.split("?")[0];
|
|
141
141
|
if (urlPath === "/" || urlPath === "/index.html") {
|
|
142
|
+
// The Preact dashboard is the default (and only) UI. The old vanilla dashboard + its
|
|
143
|
+
// ?next opt-in seam were retired in the flip; index.html IS the Preact shell now.
|
|
142
144
|
serveIndex(res);
|
|
143
145
|
return;
|
|
144
146
|
}
|
|
@@ -797,6 +799,10 @@ function handleScreen(req, res) {
|
|
|
797
799
|
if (url === "/screen/record/stop" && req.method === "POST") {
|
|
798
800
|
void (async () => {
|
|
799
801
|
const p = await stopRecording();
|
|
802
|
+
// If the viewer already left (the close-while-recording path drops the <img> before
|
|
803
|
+
// this POST lands), the recording was the only thing keeping the cast alive — reap it
|
|
804
|
+
// now so screenrecord + ffmpeg do not run on with no viewer.
|
|
805
|
+
reapLiveIfIdle();
|
|
800
806
|
const file = p ? path.basename(p) : null;
|
|
801
807
|
let sizeBytes = 0;
|
|
802
808
|
if (p) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slack/radar-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "MCP server and web dashboard for on-device debugging of the Slack Android app via ADB",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/mcp/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"clean": "rm -rf dist",
|
|
18
18
|
"prebuild": "npm run clean",
|
|
19
19
|
"copy-vendor": "node scripts/copy-vendor.mjs",
|
|
20
|
-
"build": "tsc
|
|
20
|
+
"build:web": "tsc -p tsconfig.web.json",
|
|
21
|
+
"build": "tsc && npm run build:web && node scripts/copy-public.mjs && npm run copy-vendor && chmod +x dist/mcp/index.js dist/web/bin.js",
|
|
21
22
|
"start": "node dist/mcp/index.js",
|
|
22
23
|
"start:web": "node dist/web/bin.js",
|
|
23
24
|
"watch": "tsc --watch",
|