@slack/radar-mcp 1.5.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.
Files changed (46) hide show
  1. package/README.md +5 -4
  2. package/dist/mcp/api-paths.d.ts +1 -0
  3. package/dist/mcp/api-paths.js +108 -0
  4. package/dist/mcp/db-tools.d.ts +46 -0
  5. package/dist/mcp/db-tools.js +160 -0
  6. package/dist/mcp/index.js +84 -80
  7. package/dist/mcp/logs-result.d.ts +9 -0
  8. package/dist/mcp/logs-result.js +15 -0
  9. package/dist/mcp/tools.js +70 -7
  10. package/dist/shared/android.d.ts +2 -2
  11. package/dist/shared/android.js +30 -13
  12. package/dist/shared/constants.d.ts +15 -0
  13. package/dist/shared/constants.js +24 -0
  14. package/dist/shared/db.d.ts +31 -14
  15. package/dist/shared/db.js +44 -32
  16. package/dist/shared/debug-log.d.ts +50 -0
  17. package/dist/shared/debug-log.js +108 -0
  18. package/dist/shared/screen.d.ts +35 -2
  19. package/dist/shared/screen.js +75 -6
  20. package/dist/shared/stream.d.ts +1 -1
  21. package/dist/shared/transport.d.ts +22 -4
  22. package/dist/web/bin.d.ts +16 -1
  23. package/dist/web/bin.js +133 -19
  24. package/dist/web/public/index.html +244 -1069
  25. package/dist/web/public/next/app.js +210 -0
  26. package/dist/web/public/next/db.js +286 -0
  27. package/dist/web/public/next/esc.js +13 -0
  28. package/dist/web/public/next/feed.js +608 -0
  29. package/dist/web/public/next/filters.js +120 -0
  30. package/dist/web/public/next/hooks.js +57 -0
  31. package/dist/web/public/next/html.js +10 -0
  32. package/dist/web/public/next/inspector.js +258 -0
  33. package/dist/web/public/next/registry.js +65 -0
  34. package/dist/web/public/next/screen.js +180 -0
  35. package/dist/web/public/next/spec-types.js +1 -0
  36. package/dist/web/public/next/store.js +714 -0
  37. package/dist/web/public/next/widgets.js +194 -0
  38. package/dist/web/public/vendor/README.md +25 -0
  39. package/dist/web/public/vendor/hooks.module.js +2 -0
  40. package/dist/web/public/vendor/htm.LICENSE +202 -0
  41. package/dist/web/public/vendor/htm.module.js +1 -0
  42. package/dist/web/public/vendor/preact.LICENSE +21 -0
  43. package/dist/web/public/vendor/preact.module.js +2 -0
  44. package/dist/web/server.d.ts +17 -0
  45. package/dist/web/server.js +194 -16
  46. package/package.json +6 -2
@@ -0,0 +1,180 @@
1
+ // Screen overlay: a global right-docked panel mirroring the live device screen, available over
2
+ // ANY tab. OFF by default; opens to a consent gate (the screen shows real DMs/message content).
3
+ // Ported verbatim from the PoC screen.js. Consent is enforced server-side too (/screen/* refuse
4
+ // frames before consent). Mounted as a router SIBLING (not inside the stream root), so it floats
5
+ // over the feed / db / blank views alike.
6
+ import { html } from "./html.js";
7
+ import { useState, useEffect, useRef, useCallback } from "preact/hooks";
8
+ // The MJPEG live stream runs at 20fps (measured sweet spot: 20 beats 12 on both frame
9
+ // rate and jitter; 30/60 add no frames and worsen jitter). The cache-buster ?t pins a
10
+ // fresh connection each time the <img> is (re)wired so a stale frame is never reused.
11
+ const STREAM_FPS = 20;
12
+ function streamSrc() {
13
+ return "/screen/stream?fps=" + STREAM_FPS + "&t=" + Date.now();
14
+ }
15
+ export function ScreenOverlay({ open, onClose }) {
16
+ const [caps, setCaps] = useState(null);
17
+ const [consent, setConsent] = useState(false);
18
+ const [recording, setRecording] = useState(false);
19
+ const [hint, setHint] = useState("");
20
+ const imgRef = useRef(null);
21
+ // The overlay is a permanent sibling in App (it returns null when closed but never
22
+ // unmounts), so an unmount-cleanup never fires to stop a recording on close. Track the
23
+ // live recording flag in a ref so the open-keyed effect below reads the CURRENT value
24
+ // (not a stale closure) when it tears down.
25
+ const recordingRef = useRef(false);
26
+ recordingRef.current = recording;
27
+ // Download whatever /screen/record/stop returns, then unset recording. Shared by the
28
+ // explicit stop button and the close-while-recording teardown so both behave identically.
29
+ const stopAndSave = useCallback(async () => {
30
+ const r = await (await fetch("/screen/record/stop", { method: "POST" })).json().catch(() => ({}));
31
+ setRecording(false);
32
+ if (!r.download)
33
+ return;
34
+ try {
35
+ const blob = await (await fetch(r.download)).blob();
36
+ const u = URL.createObjectURL(blob);
37
+ const a = document.createElement("a");
38
+ a.href = u;
39
+ a.download = r.file || "radar-screen.mp4";
40
+ document.body.appendChild(a);
41
+ a.click();
42
+ a.remove();
43
+ setTimeout(() => URL.revokeObjectURL(u), 10000);
44
+ }
45
+ catch {
46
+ setHint("recording saved on host: " + (r.file || ""));
47
+ }
48
+ }, []);
49
+ // Stop + save a running recording when the panel closes. Without this, closing the panel
50
+ // (or toggling ▣ Screen) mid-recording unmounts the <img> and drops the live viewer, but
51
+ // the server keeps screenrecord + ffmpeg + a growing mp4 alive (removeViewer only cleans
52
+ // up when there is no recording) with no UI left to stop them. Auto-stopping saves the
53
+ // clip the user started and lets the server tear the pipeline down.
54
+ useEffect(() => {
55
+ if (!open && recordingRef.current)
56
+ void stopAndSave();
57
+ }, [open, stopAndSave]);
58
+ const loadCaps = useCallback(async () => {
59
+ try {
60
+ const c = await (await fetch("/screen/caps?refresh=1")).json();
61
+ setCaps(c);
62
+ setConsent(!!c.consent);
63
+ }
64
+ catch {
65
+ setCaps(null);
66
+ }
67
+ }, []);
68
+ useEffect(() => {
69
+ if (open)
70
+ loadCaps();
71
+ }, [open, loadCaps]);
72
+ // wire the live <img> once consent + ffmpeg are present
73
+ useEffect(() => {
74
+ if (!open || !consent)
75
+ return;
76
+ if (caps && caps.liveCast && imgRef.current) {
77
+ imgRef.current.src = streamSrc();
78
+ setHint("capturing first frame (~3s on a foldable)…");
79
+ const id = setTimeout(() => setHint("live. ▣ Screen again to close."), 3500);
80
+ return () => clearTimeout(id);
81
+ }
82
+ }, [open, consent, caps]);
83
+ const accept = useCallback(async () => {
84
+ try {
85
+ await fetch("/screen/consent", { method: "POST" });
86
+ setConsent(true);
87
+ }
88
+ catch {
89
+ setHint("consent failed");
90
+ }
91
+ }, []);
92
+ const shot = useCallback(async () => {
93
+ if (!consent) {
94
+ setHint("accept the notice first");
95
+ return;
96
+ }
97
+ try {
98
+ const blob = await (await fetch("/screen/shot?t=" + Date.now())).blob();
99
+ const u = URL.createObjectURL(blob);
100
+ const a = document.createElement("a");
101
+ a.href = u;
102
+ a.download = "radar-screen-" + Date.now() + ".png";
103
+ document.body.appendChild(a);
104
+ a.click();
105
+ a.remove();
106
+ setTimeout(() => URL.revokeObjectURL(u), 10000);
107
+ }
108
+ catch {
109
+ setHint("screenshot failed");
110
+ }
111
+ }, [consent]);
112
+ const rec = useCallback(async () => {
113
+ if (!consent || !(caps && caps.liveCast)) {
114
+ setHint("recording needs ffmpeg + consent");
115
+ return;
116
+ }
117
+ if (!recording) {
118
+ setRecording(true);
119
+ await fetch("/screen/record/start", { method: "POST" }).catch(() => { });
120
+ }
121
+ else {
122
+ await stopAndSave();
123
+ // re-wire the live view: stopping the recording does not drop the stream, but
124
+ // re-pointing the <img> guarantees a fresh frame after the encoder handoff.
125
+ if (imgRef.current)
126
+ imgRef.current.src = streamSrc();
127
+ }
128
+ }, [consent, caps, recording, stopAndSave]);
129
+ const redetect = useCallback(async () => {
130
+ setHint("re-detecting active panel…");
131
+ const r = await (await fetch("/screen/redetect")).json().catch(() => ({}));
132
+ if (consent && caps && caps.liveCast && imgRef.current)
133
+ imgRef.current.src = streamSrc();
134
+ setHint("display: " + (r.display || "default"));
135
+ }, [consent, caps]);
136
+ if (!open)
137
+ return null;
138
+ let body;
139
+ if (!consent) {
140
+ body = html `
141
+ <div class="scconsent">
142
+ This mirrors the <b>live device screen</b>. It shows whatever is on the phone right now,
143
+ including open DMs, message content, and notifications. Review what is on screen before
144
+ sharing it. Frames are not requested until you accept, and the mirror stays on until you
145
+ restart the dashboard. If you <b>record</b>, the clip is saved to a file on this computer
146
+ (owner-only, removed when the dashboard restarts).
147
+ <button class="scaccept" onClick=${accept}>Show the screen</button>
148
+ </div>
149
+ `;
150
+ }
151
+ else if (caps && caps.liveCast) {
152
+ body = html `<img id="screenimg" ref=${imgRef} alt="device screen" onError=${() => setHint("stream dropped (device disconnected?). ⟳ to retry.")} />`;
153
+ }
154
+ else {
155
+ const cmd = caps && caps.installHint;
156
+ body = html `
157
+ <div class="scinstall">
158
+ Live mirroring needs <b>ffmpeg</b>, which is not installed.
159
+ ${cmd
160
+ ? html `<span> Install it with this command, then reopen this panel:<br /><br /><code>${cmd}</code> <button class="sccopy" onClick=${(e) => { navigator.clipboard?.writeText(cmd); e.currentTarget.textContent = "copied"; }}>copy</button></span>`
161
+ : html `<span> Install it with your system package manager (for example <code>brew install ffmpeg</code> on macOS, <code>winget install ffmpeg</code> on Windows, or <code>sudo apt install ffmpeg</code> on Debian/Ubuntu), then reopen this panel.</span>`}
162
+ </div>
163
+ <div class="scinstall">📷 Screenshot works without ffmpeg — use the button above.</div>
164
+ ${caps && caps.scrcpy ? html `<div class="scinstall">scrcpy is installed; for a native mirror window run <code>scrcpy</code> in a terminal.</div>` : null}
165
+ `;
166
+ }
167
+ return html `
168
+ <div id="screenpanel" class="on">
169
+ <div class="schead">
170
+ <span class="sctitle">Device screen</span>
171
+ <button class="scbtn" onClick=${shot} title="Full-resolution screenshot">📷</button>
172
+ <button class=${"scbtn" + (recording ? " rec" : "")} onClick=${rec} title="Record to mp4">${recording ? "■ stop" : "● rec"}</button>
173
+ <button class="scbtn" onClick=${redetect} title="Re-detect the active display">⟳</button>
174
+ <button class="scbtn" onClick=${onClose} title="Close">✕</button>
175
+ </div>
176
+ <div id="screenbody">${body}</div>
177
+ <div id="schint">${hint}</div>
178
+ </div>
179
+ `;
180
+ }
@@ -0,0 +1 @@
1
+ export {};