@deadragdoll/tellymcp 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-ru.md +19 -0
- package/README.md +19 -0
- package/TOOLS.md +206 -3
- package/dist/cli.js +109 -1
- package/dist/services/features/telegram-mcp/browser.service.js +38 -1
- package/dist/services/features/telegram-mcp/mcp-server.service.js +12 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +14 -0
- package/dist/services/features/telegram-mcp/src/app/config/env.js +13 -0
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +147 -2
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +485 -1
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +502 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +79 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +559 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +2 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
- package/docs/STANDALONE-ru.md +42 -6
- package/docs/STANDALONE.md +42 -6
- package/package.json +6 -3
- package/packages/chrome-attach-extension/dist/background.js +1326 -0
- package/packages/chrome-attach-extension/dist/icon.svg +6 -0
- package/packages/chrome-attach-extension/dist/manifest.json +36 -0
- package/packages/chrome-attach-extension/dist/options.html +312 -0
- package/packages/chrome-attach-extension/dist/options.js +593 -0
- package/packages/chrome-attach-extension/dist/popup.html +93 -0
- package/packages/chrome-attach-extension/dist/popup.js +79 -0
- package/packages/chrome-attach-extension/dist/recorder-content.js +83 -0
- package/packages/chrome-attach-extension/dist/recorder-page.js +266 -0
- package/packages/firefox-attach-extension/README.md +13 -0
- package/packages/firefox-attach-extension/dist/background.js +1242 -0
- package/packages/firefox-attach-extension/dist/icon.svg +6 -0
- package/packages/firefox-attach-extension/dist/manifest.json +56 -0
- package/packages/firefox-attach-extension/dist/options.html +312 -0
- package/packages/firefox-attach-extension/dist/options.js +527 -0
- package/packages/firefox-attach-extension/dist/popup.html +93 -0
- package/packages/firefox-attach-extension/dist/popup.js +64 -0
- package/packages/firefox-attach-extension/dist/recorder-content.js +77 -0
- package/packages/firefox-attach-extension/dist/recorder-page.js +302 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
const DEFAULT_SETTINGS = {
|
|
2
|
+
host: "127.0.0.1",
|
|
3
|
+
port: 9999,
|
|
4
|
+
attach_connection_enabled: true,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const DEFAULT_RECORDING_STATUS = {
|
|
8
|
+
active: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONNECTION_STATUS = {
|
|
12
|
+
state: "disconnected",
|
|
13
|
+
text: "Disconnected",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_INSTANCE_ID = "";
|
|
17
|
+
const ATTACHED_TAB_KEY = "attach_selected_tab";
|
|
18
|
+
const POPUP_COMMAND_KEY = "attach_popup_command";
|
|
19
|
+
const POPUP_COMMAND_RESULT_KEY = "attach_popup_command_result";
|
|
20
|
+
|
|
21
|
+
function storageGet(defaults) {
|
|
22
|
+
return browser.storage.local.get(defaults);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function storageSet(value) {
|
|
26
|
+
return browser.storage.local.set(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setStatus(message, isError = false) {
|
|
30
|
+
const status = document.getElementById("status");
|
|
31
|
+
if (!status) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
status.textContent = message || "";
|
|
35
|
+
status.style.color = isError ? "var(--danger)" : "var(--accent)";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setConnectionText(state, text) {
|
|
39
|
+
const node = document.getElementById("connection-status");
|
|
40
|
+
if (!node) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
node.textContent = text;
|
|
44
|
+
node.style.color =
|
|
45
|
+
state === "connected"
|
|
46
|
+
? "var(--accent)"
|
|
47
|
+
: state === "connecting"
|
|
48
|
+
? "var(--warn)"
|
|
49
|
+
: "var(--soft)";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setMeta(id, text) {
|
|
53
|
+
const node = document.getElementById(id);
|
|
54
|
+
if (!node) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
node.textContent = text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setAttachedText(attachedTab) {
|
|
61
|
+
const node = document.getElementById("attached-tab");
|
|
62
|
+
if (!node) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
node.textContent = attachedTab?.title
|
|
66
|
+
? `Attached: ${attachedTab.title}`
|
|
67
|
+
: "Attached: -";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setRecordingText(recordingStatus) {
|
|
71
|
+
const node = document.getElementById("recording");
|
|
72
|
+
if (!node) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!recordingStatus?.active || !recordingStatus.recording) {
|
|
76
|
+
node.textContent = "Recording: inactive";
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
node.textContent = `Recording: ${recordingStatus.recording.bundle_dir_name}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setRecordingTabText(recordingStatus) {
|
|
83
|
+
const node = document.getElementById("recording-tab");
|
|
84
|
+
if (!node) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!recordingStatus?.active || !recordingStatus.recording) {
|
|
88
|
+
node.textContent = "Recording tab: -";
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
node.textContent = `Recording tab: ${recordingStatus.recording.tab_title || `Tab ${recordingStatus.recording.tab_id}`}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setRecordingScopeText(recordingStatus, currentInstanceId) {
|
|
95
|
+
const node = document.getElementById("recording-scope");
|
|
96
|
+
if (!node) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!recordingStatus?.active || !recordingStatus.recording) {
|
|
100
|
+
node.textContent = "";
|
|
101
|
+
node.style.color = "var(--soft)";
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
currentInstanceId &&
|
|
106
|
+
recordingStatus.recording.instance_id &&
|
|
107
|
+
recordingStatus.recording.instance_id !== currentInstanceId
|
|
108
|
+
) {
|
|
109
|
+
node.textContent = "Recording is active in another browser instance.";
|
|
110
|
+
node.style.color = "var(--warn)";
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
node.textContent = "Recording belongs to this browser instance.";
|
|
114
|
+
node.style.color = "var(--soft)";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function setActionStatus(message, tone = "neutral") {
|
|
118
|
+
const node = document.getElementById("action-status");
|
|
119
|
+
if (!node) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
node.textContent = message || "";
|
|
123
|
+
node.style.color =
|
|
124
|
+
tone === "success"
|
|
125
|
+
? "var(--accent)"
|
|
126
|
+
: tone === "error"
|
|
127
|
+
? "var(--danger)"
|
|
128
|
+
: "var(--soft)";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function updateConnectionButtons(status, enabled) {
|
|
132
|
+
const toggleButton = document.getElementById("connection-toggle-button");
|
|
133
|
+
if (!toggleButton) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const connected = status?.state === "connected";
|
|
137
|
+
const connecting = status?.state === "connecting";
|
|
138
|
+
const shouldDisconnect = enabled !== false && (connected || connecting);
|
|
139
|
+
toggleButton.textContent = shouldDisconnect ? "Disconnect" : "Connect";
|
|
140
|
+
toggleButton.disabled = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderTabs(tabs, attachedTabId, recordingStatus, connected, currentInstanceId) {
|
|
144
|
+
const container = document.getElementById("tabs");
|
|
145
|
+
if (!container) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
container.textContent = "";
|
|
149
|
+
const recordingTabId = recordingStatus?.active === true ? recordingStatus.recording?.tab_id : null;
|
|
150
|
+
|
|
151
|
+
for (const tab of tabs) {
|
|
152
|
+
const row = document.createElement("div");
|
|
153
|
+
row.className = "tab-row";
|
|
154
|
+
|
|
155
|
+
const isAttached = tab.id === attachedTabId;
|
|
156
|
+
const isRecording = tab.id === recordingTabId;
|
|
157
|
+
const isLockedByOtherInstance =
|
|
158
|
+
recordingStatus?.active === true &&
|
|
159
|
+
Boolean(recordingStatus?.recording?.instance_id) &&
|
|
160
|
+
Boolean(currentInstanceId) &&
|
|
161
|
+
recordingStatus.recording.instance_id !== currentInstanceId;
|
|
162
|
+
|
|
163
|
+
const button = document.createElement("button");
|
|
164
|
+
button.type = "button";
|
|
165
|
+
button.className = "tab";
|
|
166
|
+
if (isAttached) {
|
|
167
|
+
button.classList.add("active");
|
|
168
|
+
}
|
|
169
|
+
if (tab.active === true) {
|
|
170
|
+
button.classList.add("current");
|
|
171
|
+
}
|
|
172
|
+
if (isRecording) {
|
|
173
|
+
button.classList.add("recording");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const head = document.createElement("div");
|
|
177
|
+
head.className = "tab-head";
|
|
178
|
+
|
|
179
|
+
const title = document.createElement("div");
|
|
180
|
+
title.className = "tab-title";
|
|
181
|
+
title.textContent = tab.title || tab.url || `Tab ${tab.id}`;
|
|
182
|
+
|
|
183
|
+
const badges = document.createElement("div");
|
|
184
|
+
badges.className = "tab-badges";
|
|
185
|
+
|
|
186
|
+
if (isAttached) {
|
|
187
|
+
const attachedBadge = document.createElement("span");
|
|
188
|
+
attachedBadge.className = "badge attached";
|
|
189
|
+
attachedBadge.textContent = "Attached";
|
|
190
|
+
badges.append(attachedBadge);
|
|
191
|
+
}
|
|
192
|
+
if (isRecording) {
|
|
193
|
+
const recordingBadge = document.createElement("span");
|
|
194
|
+
recordingBadge.className = "badge recording";
|
|
195
|
+
recordingBadge.textContent = "Recording";
|
|
196
|
+
badges.append(recordingBadge);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const url = document.createElement("div");
|
|
200
|
+
url.className = "tab-url";
|
|
201
|
+
url.textContent = tab.url || "";
|
|
202
|
+
|
|
203
|
+
head.append(title, badges);
|
|
204
|
+
button.append(head, url);
|
|
205
|
+
button.addEventListener("click", () => {
|
|
206
|
+
void attachTab(tab.id);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const recordButton = document.createElement("button");
|
|
210
|
+
recordButton.type = "button";
|
|
211
|
+
recordButton.className = "secondary tab-record-button";
|
|
212
|
+
recordButton.textContent = isRecording ? "Stop" : "Record";
|
|
213
|
+
recordButton.disabled =
|
|
214
|
+
!connected ||
|
|
215
|
+
isLockedByOtherInstance ||
|
|
216
|
+
(recordingStatus?.active === true && !isRecording);
|
|
217
|
+
recordButton.addEventListener("click", (event) => {
|
|
218
|
+
event.stopPropagation();
|
|
219
|
+
if (isLockedByOtherInstance) {
|
|
220
|
+
setActionStatus("Recording is already active in another browser instance.", "error");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (isRecording) {
|
|
224
|
+
void stopRecording();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
void startRecordingForTab(tab.id);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
row.append(button, recordButton);
|
|
231
|
+
container.append(row);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function requestRecordingStatus() {
|
|
236
|
+
try {
|
|
237
|
+
return await sendPopupCommand("attach_recording_status");
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
error: error instanceof Error ? error.message : String(error),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function refreshRecordingStatus() {
|
|
247
|
+
const result = await requestRecordingStatus();
|
|
248
|
+
if (!result?.ok) {
|
|
249
|
+
setActionStatus(result?.error || "Could not refresh recording status.", "error");
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function attachTab(tabId, options = {}) {
|
|
255
|
+
const { suppressSuccessStatus = false } = options;
|
|
256
|
+
setActionStatus("Attaching tab...", "neutral");
|
|
257
|
+
let result;
|
|
258
|
+
try {
|
|
259
|
+
result = await sendPopupCommand("attach_tab_selected", {
|
|
260
|
+
tab_id: tabId,
|
|
261
|
+
});
|
|
262
|
+
} catch (error) {
|
|
263
|
+
result = {
|
|
264
|
+
ok: false,
|
|
265
|
+
error: error instanceof Error ? error.message : String(error),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (!result?.ok) {
|
|
269
|
+
setActionStatus(result?.error || "Attach failed.", "error");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!suppressSuccessStatus) {
|
|
273
|
+
setActionStatus(`Attached tab ${result.tab?.tab_id}.`, "success");
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function startRecording() {
|
|
279
|
+
setActionStatus("Starting recording...", "neutral");
|
|
280
|
+
let result;
|
|
281
|
+
try {
|
|
282
|
+
result = await sendPopupCommand("attach_recording_start");
|
|
283
|
+
} catch (error) {
|
|
284
|
+
result = {
|
|
285
|
+
ok: false,
|
|
286
|
+
error: error instanceof Error ? error.message : String(error),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (!result?.ok) {
|
|
290
|
+
setActionStatus(result?.error || "Could not start recording.", "error");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
setActionStatus(
|
|
294
|
+
result?.recording?.bundle_dir_name
|
|
295
|
+
? `Recording started: ${result.recording.bundle_dir_name}`
|
|
296
|
+
: "Recording started.",
|
|
297
|
+
"success",
|
|
298
|
+
);
|
|
299
|
+
await refreshRecordingStatus();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function stopRecording() {
|
|
303
|
+
setActionStatus("Stopping recording...", "neutral");
|
|
304
|
+
let result;
|
|
305
|
+
try {
|
|
306
|
+
result = await sendPopupCommand("attach_recording_stop");
|
|
307
|
+
} catch (error) {
|
|
308
|
+
result = {
|
|
309
|
+
ok: false,
|
|
310
|
+
error: error instanceof Error ? error.message : String(error),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (!result?.ok) {
|
|
314
|
+
setActionStatus(result?.error || "Could not stop recording.", "error");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
setActionStatus("Recording stopped.", "success");
|
|
318
|
+
await refreshRecordingStatus();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function startRecordingForTab(tabId) {
|
|
322
|
+
const stored = await storageGet({
|
|
323
|
+
attach_connection_status: DEFAULT_CONNECTION_STATUS,
|
|
324
|
+
attach_recording_status: DEFAULT_RECORDING_STATUS,
|
|
325
|
+
attach_instance_id: DEFAULT_INSTANCE_ID,
|
|
326
|
+
});
|
|
327
|
+
const currentInstanceId = stored.attach_instance_id || stored.attach_connection_status?.instance_id;
|
|
328
|
+
const recordingStatus = stored.attach_recording_status ?? DEFAULT_RECORDING_STATUS;
|
|
329
|
+
if (
|
|
330
|
+
recordingStatus?.active === true &&
|
|
331
|
+
recordingStatus.recording?.instance_id &&
|
|
332
|
+
currentInstanceId &&
|
|
333
|
+
recordingStatus.recording.instance_id !== currentInstanceId
|
|
334
|
+
) {
|
|
335
|
+
setActionStatus("Recording is already active in another browser instance.", "error");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const attachResult = await attachTab(tabId, { suppressSuccessStatus: true });
|
|
339
|
+
if (!attachResult?.ok) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
await startRecording();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function injectSelectedFile() {
|
|
346
|
+
const fileInput = document.getElementById("inject-file");
|
|
347
|
+
const namespaceInput = document.getElementById("inject-namespace");
|
|
348
|
+
const file = fileInput?.files?.[0];
|
|
349
|
+
if (!file) {
|
|
350
|
+
setActionStatus("Choose a script file first.", "error");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const source = await file.text();
|
|
354
|
+
if (!source.trim()) {
|
|
355
|
+
setActionStatus("Selected script file is empty.", "error");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
setActionStatus(`Injecting ${file.name}...`, "neutral");
|
|
359
|
+
let result;
|
|
360
|
+
try {
|
|
361
|
+
result = await sendPopupCommand("attach_inject_script", {
|
|
362
|
+
source,
|
|
363
|
+
namespace: namespaceInput?.value?.trim() || "TELLY",
|
|
364
|
+
file_name: file.name,
|
|
365
|
+
});
|
|
366
|
+
} catch (error) {
|
|
367
|
+
result = {
|
|
368
|
+
ok: false,
|
|
369
|
+
error: error instanceof Error ? error.message : String(error),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (!result?.ok) {
|
|
373
|
+
setActionStatus(result?.error || "Script injection failed.", "error");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
setActionStatus(`Injected ${file.name}.`, "success");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function setConnectionEnabled(enabled) {
|
|
380
|
+
await storageSet({
|
|
381
|
+
attach_connection_enabled: enabled,
|
|
382
|
+
});
|
|
383
|
+
setActionStatus(enabled ? "Connect requested." : "Disconnected.", "success");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function waitForStorageResult(commandId, timeoutMs = 15000) {
|
|
387
|
+
return new Promise((resolve, reject) => {
|
|
388
|
+
const timer = setTimeout(() => {
|
|
389
|
+
browser.storage.onChanged.removeListener(listener);
|
|
390
|
+
reject(new Error("Request timed out."));
|
|
391
|
+
}, timeoutMs);
|
|
392
|
+
function listener(changes, areaName) {
|
|
393
|
+
if (areaName !== "local" || !changes[POPUP_COMMAND_RESULT_KEY]) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const nextValue = changes[POPUP_COMMAND_RESULT_KEY].newValue;
|
|
397
|
+
if (!nextValue || nextValue.command_id !== commandId) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
browser.storage.onChanged.removeListener(listener);
|
|
402
|
+
resolve(nextValue.result);
|
|
403
|
+
}
|
|
404
|
+
browser.storage.onChanged.addListener(listener);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function sendPopupCommand(type, payload = {}, timeoutMs = 15000) {
|
|
409
|
+
const commandId = `options-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
410
|
+
await storageSet({
|
|
411
|
+
[POPUP_COMMAND_KEY]: {
|
|
412
|
+
command_id: commandId,
|
|
413
|
+
type,
|
|
414
|
+
...payload,
|
|
415
|
+
at: new Date().toISOString(),
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
try {
|
|
419
|
+
return await waitForStorageResult(commandId, timeoutMs);
|
|
420
|
+
} finally {
|
|
421
|
+
try {
|
|
422
|
+
await storageSet({
|
|
423
|
+
[POPUP_COMMAND_KEY]: null,
|
|
424
|
+
});
|
|
425
|
+
} catch {
|
|
426
|
+
// ignore
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function saveSettings(event) {
|
|
432
|
+
event.preventDefault();
|
|
433
|
+
const host = document.getElementById("host").value.trim() || DEFAULT_SETTINGS.host;
|
|
434
|
+
const portRaw = document.getElementById("port").value.trim();
|
|
435
|
+
const port = Number(portRaw || DEFAULT_SETTINGS.port);
|
|
436
|
+
|
|
437
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
438
|
+
setStatus("Port must be an integer between 1 and 65535.", true);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
await storageSet({
|
|
443
|
+
host,
|
|
444
|
+
port,
|
|
445
|
+
});
|
|
446
|
+
setStatus("Settings saved.");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function render() {
|
|
450
|
+
const [stored, tabs] = await Promise.all([
|
|
451
|
+
storageGet({
|
|
452
|
+
...DEFAULT_SETTINGS,
|
|
453
|
+
attach_connection_status: DEFAULT_CONNECTION_STATUS,
|
|
454
|
+
attach_connection_enabled: true,
|
|
455
|
+
[ATTACHED_TAB_KEY]: null,
|
|
456
|
+
attach_recording_status: DEFAULT_RECORDING_STATUS,
|
|
457
|
+
attach_instance_id: DEFAULT_INSTANCE_ID,
|
|
458
|
+
}),
|
|
459
|
+
browser.tabs.query({ currentWindow: true }),
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
document.getElementById("host").value = stored.host ?? DEFAULT_SETTINGS.host;
|
|
463
|
+
document.getElementById("port").value = String(stored.port ?? DEFAULT_SETTINGS.port);
|
|
464
|
+
const status = stored.attach_connection_status ?? DEFAULT_CONNECTION_STATUS;
|
|
465
|
+
const recordingStatus = stored.attach_recording_status ?? DEFAULT_RECORDING_STATUS;
|
|
466
|
+
const currentInstanceId = stored.attach_instance_id || status.instance_id;
|
|
467
|
+
|
|
468
|
+
setConnectionText(status.state, status.text || DEFAULT_CONNECTION_STATUS.text);
|
|
469
|
+
updateConnectionButtons(status, stored.attach_connection_enabled);
|
|
470
|
+
setMeta("endpoint", `Endpoint: ${stored.host}:${stored.port}`);
|
|
471
|
+
setMeta(
|
|
472
|
+
"session",
|
|
473
|
+
`Session: ${status.session_label || status.session_id || "-"}`,
|
|
474
|
+
);
|
|
475
|
+
setAttachedText(stored[ATTACHED_TAB_KEY]);
|
|
476
|
+
setRecordingText(recordingStatus);
|
|
477
|
+
setRecordingTabText(recordingStatus);
|
|
478
|
+
setRecordingScopeText(recordingStatus, currentInstanceId);
|
|
479
|
+
renderTabs(
|
|
480
|
+
tabs,
|
|
481
|
+
stored[ATTACHED_TAB_KEY]?.tab_id,
|
|
482
|
+
recordingStatus,
|
|
483
|
+
status.state === "connected",
|
|
484
|
+
currentInstanceId,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
document
|
|
489
|
+
.getElementById("settings-form")
|
|
490
|
+
.addEventListener("submit", (event) => {
|
|
491
|
+
void saveSettings(event);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
document.getElementById("connection-toggle-button").addEventListener("click", async () => {
|
|
495
|
+
const stored = await storageGet({
|
|
496
|
+
attach_connection_status: DEFAULT_CONNECTION_STATUS,
|
|
497
|
+
attach_connection_enabled: true,
|
|
498
|
+
});
|
|
499
|
+
const status = stored.attach_connection_status ?? DEFAULT_CONNECTION_STATUS;
|
|
500
|
+
const enabled = stored.attach_connection_enabled;
|
|
501
|
+
const connected = status.state === "connected" || status.state === "connecting";
|
|
502
|
+
await setConnectionEnabled(!(enabled !== false && connected));
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
browser.storage.onChanged.addListener((changes, areaName) => {
|
|
506
|
+
if (areaName !== "local") {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (
|
|
510
|
+
changes.attach_connection_status ||
|
|
511
|
+
changes.attach_instance_id ||
|
|
512
|
+
changes.host ||
|
|
513
|
+
changes.port ||
|
|
514
|
+
changes.attach_connection_enabled ||
|
|
515
|
+
changes.attach_recording_status ||
|
|
516
|
+
changes[ATTACHED_TAB_KEY]
|
|
517
|
+
) {
|
|
518
|
+
void render();
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
document.getElementById("inject-script-button").addEventListener("click", () => {
|
|
523
|
+
void injectSelectedFile();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
void render();
|
|
527
|
+
void refreshRecordingStatus();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>TellyMCP Firefox Attach</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
font-family: ui-monospace, "SFMono-Regular", "Cascadia Code", "Fira Code", monospace;
|
|
11
|
+
--bg-start: #fffdf8;
|
|
12
|
+
--bg-end: #f3efe2;
|
|
13
|
+
--card-bg: rgba(255, 255, 255, 0.88);
|
|
14
|
+
--card-border: #d7cfbb;
|
|
15
|
+
--text: #171717;
|
|
16
|
+
--muted: #4b4b4b;
|
|
17
|
+
--soft: #5a5a5a;
|
|
18
|
+
--accent: #0a6e2f;
|
|
19
|
+
--warn: #8a5a00;
|
|
20
|
+
--button-border: #171717;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@media (prefers-color-scheme: dark) {
|
|
24
|
+
:root {
|
|
25
|
+
--bg-start: #171a1d;
|
|
26
|
+
--bg-end: #101214;
|
|
27
|
+
--card-bg: rgba(28, 31, 35, 0.92);
|
|
28
|
+
--card-border: #323841;
|
|
29
|
+
--text: #f3f5f7;
|
|
30
|
+
--muted: #c5ccd3;
|
|
31
|
+
--soft: #98a2ad;
|
|
32
|
+
--accent: #71d08d;
|
|
33
|
+
--warn: #f3bf63;
|
|
34
|
+
--button-border: #56606b;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
body {
|
|
39
|
+
margin: 0;
|
|
40
|
+
width: 320px;
|
|
41
|
+
padding: 16px;
|
|
42
|
+
background: linear-gradient(180deg, var(--bg-start) 0%, var(--bg-end) 100%);
|
|
43
|
+
color: var(--text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.card {
|
|
47
|
+
border: 1px solid var(--card-border);
|
|
48
|
+
background: var(--card-bg);
|
|
49
|
+
padding: 14px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
h1 {
|
|
53
|
+
margin: 0 0 10px;
|
|
54
|
+
font-size: 18px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.status {
|
|
58
|
+
font-size: 14px;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
margin: 0 0 10px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.meta {
|
|
64
|
+
display: grid;
|
|
65
|
+
gap: 6px;
|
|
66
|
+
font-size: 12px;
|
|
67
|
+
color: var(--muted);
|
|
68
|
+
margin-bottom: 14px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
button {
|
|
72
|
+
font: inherit;
|
|
73
|
+
padding: 9px 14px;
|
|
74
|
+
border: 1px solid var(--button-border);
|
|
75
|
+
background: transparent;
|
|
76
|
+
color: var(--text);
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<div class="card">
|
|
83
|
+
<h1>TellyMCP Firefox Attach</h1>
|
|
84
|
+
<p id="connection-status" class="status">Connecting...</p>
|
|
85
|
+
<div class="meta">
|
|
86
|
+
<div id="session">Session: -</div>
|
|
87
|
+
<div id="attached-tab">Attached: -</div>
|
|
88
|
+
</div>
|
|
89
|
+
<button id="open-settings" type="button">Open Control Panel</button>
|
|
90
|
+
</div>
|
|
91
|
+
<script src="popup.js"></script>
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const DEFAULT_CONNECTION_STATUS = {
|
|
2
|
+
state: "disconnected",
|
|
3
|
+
text: "Disconnected",
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const ATTACHED_TAB_KEY = "attach_selected_tab";
|
|
7
|
+
|
|
8
|
+
function setStatus(state, text) {
|
|
9
|
+
const node = document.getElementById("connection-status");
|
|
10
|
+
if (!node) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
node.textContent = text;
|
|
14
|
+
node.style.color =
|
|
15
|
+
state === "connected"
|
|
16
|
+
? "var(--accent)"
|
|
17
|
+
: state === "connecting"
|
|
18
|
+
? "var(--warn)"
|
|
19
|
+
: "var(--soft)";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setText(id, text) {
|
|
23
|
+
const node = document.getElementById(id);
|
|
24
|
+
if (!node) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
node.textContent = text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function render() {
|
|
31
|
+
const stored = await browser.storage.local.get({
|
|
32
|
+
attach_connection_status: DEFAULT_CONNECTION_STATUS,
|
|
33
|
+
[ATTACHED_TAB_KEY]: null,
|
|
34
|
+
});
|
|
35
|
+
const status = stored.attach_connection_status ?? DEFAULT_CONNECTION_STATUS;
|
|
36
|
+
setStatus(status.state, status.text || DEFAULT_CONNECTION_STATUS.text);
|
|
37
|
+
setText(
|
|
38
|
+
"session",
|
|
39
|
+
`Session: ${status.session_label || status.session_id || "-"}`,
|
|
40
|
+
);
|
|
41
|
+
setText(
|
|
42
|
+
"attached-tab",
|
|
43
|
+
stored[ATTACHED_TAB_KEY]?.title
|
|
44
|
+
? `Attached: ${stored[ATTACHED_TAB_KEY].title}`
|
|
45
|
+
: "Attached: -",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
document.getElementById("open-settings").addEventListener("click", () => {
|
|
50
|
+
void browser.tabs.create({
|
|
51
|
+
url: browser.runtime.getURL("options.html"),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
browser.storage.onChanged.addListener((changes, areaName) => {
|
|
56
|
+
if (areaName !== "local") {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (changes.attach_connection_status || changes[ATTACHED_TAB_KEY]) {
|
|
60
|
+
void render();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
void render();
|