@deadragdoll/tellymcp 0.0.12 → 0.0.14
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/.env.example.client +5 -1
- package/.env.example.gateway +16 -1
- package/CHANGELOG.md +30 -0
- package/README-ru.md +33 -0
- package/README.md +33 -0
- package/TOOLS.md +384 -9
- package/config/templates/env.both.template +15 -2
- package/config/templates/env.client.template +5 -2
- package/config/templates/env.gateway.template +15 -2
- package/dist/cli.js +137 -2
- package/dist/services/features/telegram-mcp/browser.service.js +56 -1
- package/dist/services/features/telegram-mcp/file-content.service.js +94 -0
- package/dist/services/features/telegram-mcp/gateway-delivery.service.js +5 -1
- package/dist/services/features/telegram-mcp/gateway-socket.service.js +43 -11
- package/dist/services/features/telegram-mcp/mcp-http.service.js +1 -0
- package/dist/services/features/telegram-mcp/mcp-server.service.js +30 -0
- package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +23 -1
- package/dist/services/features/telegram-mcp/src/app/config/env.js +132 -2
- package/dist/services/features/telegram-mcp/src/app/http.js +139 -99
- package/dist/services/features/telegram-mcp/src/app/oauthFacade.js +642 -0
- package/dist/services/features/telegram-mcp/src/app/webapp/assets.js +151 -170
- package/dist/services/features/telegram-mcp/src/app/webapp/auth.js +96 -99
- package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +205 -17
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachActiveTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserAttachTabTool.js +28 -0
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
- package/dist/services/features/telegram-mcp/src/features/browser/model/browserDetachTabTool.js +28 -0
- 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 +890 -23
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +536 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +80 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +736 -0
- package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +188 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/collaborationService.js +2 -0
- package/dist/services/features/telegram-mcp/src/features/collaboration/model/sendPartnerFileService.js +6 -3
- package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +4 -1
- package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +33 -35
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileListTool.js +33 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileService.js +327 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/getFileTool.js +81 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/temporaryFileLinkStore.js +307 -0
- package/dist/services/features/telegram-mcp/src/features/file-content/model/workspaceFilePolicy.js +115 -0
- package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +5 -1
- package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +28 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +3 -0
- package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportFileHandoffActions.js +6 -3
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/client.js +29 -6
- package/dist/services/features/telegram-mcp/src/shared/integrations/terminal/ptyRegistry.js +100 -2
- package/dist/services/features/telegram-mcp/src/shared/lib/bodyLimits.js +63 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/gatewayAuth.js +13 -0
- package/dist/services/features/telegram-mcp/src/shared/lib/time/localTimestamp.js +21 -0
- package/docs/CHAT_CONNECTOR.md +134 -0
- package/docs/STANDALONE-ru.md +55 -6
- package/docs/STANDALONE.md +55 -6
- package/package.json +10 -5
- package/packages/chrome-attach-extension/dist/background.js +1735 -0
- package/packages/chrome-attach-extension/dist/icon.svg +6 -0
- package/packages/chrome-attach-extension/dist/manifest.json +37 -0
- package/packages/chrome-attach-extension/dist/options.html +312 -0
- package/packages/chrome-attach-extension/dist/options.js +606 -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 +96 -0
- package/packages/chrome-attach-extension/dist/recorder-page.js +282 -0
- package/packages/firefox-attach-extension/README.md +13 -0
- package/packages/firefox-attach-extension/dist/background.js +1622 -0
- package/packages/firefox-attach-extension/dist/icon.svg +6 -0
- package/packages/firefox-attach-extension/dist/manifest.json +44 -0
- package/packages/firefox-attach-extension/dist/options.html +312 -0
- package/packages/firefox-attach-extension/dist/options.js +540 -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 +90 -0
- package/packages/firefox-attach-extension/dist/recorder-page.js +318 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
const browser = globalThis.browser ?? globalThis.chrome;
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SETTINGS = {
|
|
4
|
+
host: "127.0.0.1",
|
|
5
|
+
port: 9999,
|
|
6
|
+
attach_connection_enabled: true,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const DEFAULT_RECORDING_STATUS = {
|
|
10
|
+
active: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CONNECTION_STATUS = {
|
|
14
|
+
state: "disconnected",
|
|
15
|
+
text: "Disconnected",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DEFAULT_INSTANCE_ID = "";
|
|
19
|
+
const ATTACHED_TAB_KEY = "attach_selected_tab";
|
|
20
|
+
const POPUP_COMMAND_KEY = "attach_popup_command";
|
|
21
|
+
const POPUP_COMMAND_RESULT_KEY = "attach_popup_command_result";
|
|
22
|
+
const CONTROL_PANEL_PORT_NAME = "telly-control-panel";
|
|
23
|
+
|
|
24
|
+
let controlPanelPort = null;
|
|
25
|
+
let keepAliveTimer = null;
|
|
26
|
+
|
|
27
|
+
function padNumber(value, length = 2) {
|
|
28
|
+
return String(value).padStart(length, "0");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatLocalTimestamp(date = new Date()) {
|
|
32
|
+
return `${date.getFullYear()}-${padNumber(date.getMonth() + 1)}-${padNumber(date.getDate())}T${padNumber(
|
|
33
|
+
date.getHours(),
|
|
34
|
+
)}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}.${padNumber(
|
|
35
|
+
date.getMilliseconds(),
|
|
36
|
+
3,
|
|
37
|
+
)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function storageGet(defaults) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
browser.storage.local.get(defaults, (result) => {
|
|
43
|
+
const error = browser.runtime?.lastError;
|
|
44
|
+
if (error) {
|
|
45
|
+
reject(new Error(error.message));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
resolve(result);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function storageSet(value) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
browser.storage.local.set(value, () => {
|
|
56
|
+
const error = browser.runtime?.lastError;
|
|
57
|
+
if (error) {
|
|
58
|
+
reject(new Error(error.message));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function queryTabs(queryInfo) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
browser.tabs.query(queryInfo, (tabs) => {
|
|
69
|
+
const error = browser.runtime?.lastError;
|
|
70
|
+
if (error) {
|
|
71
|
+
reject(new Error(error.message));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
resolve(tabs || []);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setStatus(message, isError = false) {
|
|
80
|
+
const status = document.getElementById("status");
|
|
81
|
+
if (!status) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
status.textContent = message || "";
|
|
85
|
+
status.style.color = isError ? "var(--danger)" : "var(--accent)";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function setConnectionText(state, text) {
|
|
89
|
+
const node = document.getElementById("connection-status");
|
|
90
|
+
if (!node) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
node.textContent = text;
|
|
94
|
+
node.style.color =
|
|
95
|
+
state === "connected"
|
|
96
|
+
? "var(--accent)"
|
|
97
|
+
: state === "connecting"
|
|
98
|
+
? "var(--warn)"
|
|
99
|
+
: "var(--soft)";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setMeta(id, text) {
|
|
103
|
+
const node = document.getElementById(id);
|
|
104
|
+
if (!node) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
node.textContent = text;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setAttachedText(attachedTab) {
|
|
111
|
+
const node = document.getElementById("attached-tab");
|
|
112
|
+
if (!node) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
node.textContent = attachedTab?.title
|
|
116
|
+
? `Attached: ${attachedTab.title}`
|
|
117
|
+
: "Attached: -";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function setRecordingText(recordingStatus) {
|
|
121
|
+
const node = document.getElementById("recording");
|
|
122
|
+
if (!node) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!recordingStatus?.active || !recordingStatus.recording) {
|
|
126
|
+
node.textContent = "Recording: inactive";
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
node.textContent = `Recording: ${recordingStatus.recording.bundle_dir_name}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setRecordingTabText(recordingStatus) {
|
|
133
|
+
const node = document.getElementById("recording-tab");
|
|
134
|
+
if (!node) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!recordingStatus?.active || !recordingStatus.recording) {
|
|
138
|
+
node.textContent = "Recording tab: -";
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
node.textContent = `Recording tab: ${recordingStatus.recording.tab_title || `Tab ${recordingStatus.recording.tab_id}`}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setRecordingScopeText(recordingStatus, currentInstanceId) {
|
|
145
|
+
const node = document.getElementById("recording-scope");
|
|
146
|
+
if (!node) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!recordingStatus?.active || !recordingStatus.recording) {
|
|
150
|
+
node.textContent = "";
|
|
151
|
+
node.style.color = "var(--soft)";
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
currentInstanceId &&
|
|
156
|
+
recordingStatus.recording.instance_id &&
|
|
157
|
+
recordingStatus.recording.instance_id !== currentInstanceId
|
|
158
|
+
) {
|
|
159
|
+
node.textContent = "Recording is active in another browser instance.";
|
|
160
|
+
node.style.color = "var(--warn)";
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
node.textContent = "Recording belongs to this browser instance.";
|
|
164
|
+
node.style.color = "var(--soft)";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function setActionStatus(message, tone = "neutral") {
|
|
168
|
+
const node = document.getElementById("action-status");
|
|
169
|
+
if (!node) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
node.textContent = message || "";
|
|
173
|
+
node.style.color =
|
|
174
|
+
tone === "success"
|
|
175
|
+
? "var(--accent)"
|
|
176
|
+
: tone === "error"
|
|
177
|
+
? "var(--danger)"
|
|
178
|
+
: "var(--soft)";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function updateConnectionButtons(status, enabled) {
|
|
182
|
+
const toggleButton = document.getElementById("connection-toggle-button");
|
|
183
|
+
if (!toggleButton) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const connected = status?.state === "connected";
|
|
187
|
+
const connecting = status?.state === "connecting";
|
|
188
|
+
const shouldDisconnect = enabled !== false && (connected || connecting);
|
|
189
|
+
toggleButton.textContent = shouldDisconnect ? "Disconnect" : "Connect";
|
|
190
|
+
toggleButton.disabled = false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderTabs(tabs, attachedTabId, recordingStatus, connected, currentInstanceId) {
|
|
194
|
+
const container = document.getElementById("tabs");
|
|
195
|
+
if (!container) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
container.textContent = "";
|
|
199
|
+
const recordingTabId = recordingStatus?.active === true ? recordingStatus.recording?.tab_id : null;
|
|
200
|
+
|
|
201
|
+
for (const tab of tabs) {
|
|
202
|
+
const row = document.createElement("div");
|
|
203
|
+
row.className = "tab-row";
|
|
204
|
+
|
|
205
|
+
const isAttached = tab.id === attachedTabId;
|
|
206
|
+
const isRecording = tab.id === recordingTabId;
|
|
207
|
+
const isLockedByOtherInstance =
|
|
208
|
+
recordingStatus?.active === true &&
|
|
209
|
+
Boolean(recordingStatus?.recording?.instance_id) &&
|
|
210
|
+
Boolean(currentInstanceId) &&
|
|
211
|
+
recordingStatus.recording.instance_id !== currentInstanceId;
|
|
212
|
+
|
|
213
|
+
const button = document.createElement("button");
|
|
214
|
+
button.type = "button";
|
|
215
|
+
button.className = "tab";
|
|
216
|
+
if (isAttached) {
|
|
217
|
+
button.classList.add("active");
|
|
218
|
+
}
|
|
219
|
+
if (tab.active === true) {
|
|
220
|
+
button.classList.add("current");
|
|
221
|
+
}
|
|
222
|
+
if (isRecording) {
|
|
223
|
+
button.classList.add("recording");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const head = document.createElement("div");
|
|
227
|
+
head.className = "tab-head";
|
|
228
|
+
|
|
229
|
+
const title = document.createElement("div");
|
|
230
|
+
title.className = "tab-title";
|
|
231
|
+
title.textContent = tab.title || tab.url || `Tab ${tab.id}`;
|
|
232
|
+
|
|
233
|
+
const badges = document.createElement("div");
|
|
234
|
+
badges.className = "tab-badges";
|
|
235
|
+
|
|
236
|
+
if (isAttached) {
|
|
237
|
+
const attachedBadge = document.createElement("span");
|
|
238
|
+
attachedBadge.className = "badge attached";
|
|
239
|
+
attachedBadge.textContent = "Attached";
|
|
240
|
+
badges.append(attachedBadge);
|
|
241
|
+
}
|
|
242
|
+
if (isRecording) {
|
|
243
|
+
const recordingBadge = document.createElement("span");
|
|
244
|
+
recordingBadge.className = "badge recording";
|
|
245
|
+
recordingBadge.textContent = "Recording";
|
|
246
|
+
badges.append(recordingBadge);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const url = document.createElement("div");
|
|
250
|
+
url.className = "tab-url";
|
|
251
|
+
url.textContent = tab.url || "";
|
|
252
|
+
|
|
253
|
+
head.append(title, badges);
|
|
254
|
+
button.append(head, url);
|
|
255
|
+
button.addEventListener("click", () => {
|
|
256
|
+
void attachTab(tab.id);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const recordButton = document.createElement("button");
|
|
260
|
+
recordButton.type = "button";
|
|
261
|
+
recordButton.className = "secondary tab-record-button";
|
|
262
|
+
recordButton.textContent = isRecording ? "Stop" : "Record";
|
|
263
|
+
recordButton.disabled =
|
|
264
|
+
!connected ||
|
|
265
|
+
isLockedByOtherInstance ||
|
|
266
|
+
(recordingStatus?.active === true && !isRecording);
|
|
267
|
+
recordButton.addEventListener("click", (event) => {
|
|
268
|
+
event.stopPropagation();
|
|
269
|
+
if (isLockedByOtherInstance) {
|
|
270
|
+
setActionStatus("Recording is already active in another browser instance.", "error");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (isRecording) {
|
|
274
|
+
void stopRecording();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
void startRecordingForTab(tab.id);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
row.append(button, recordButton);
|
|
281
|
+
container.append(row);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function startControlPanelPort() {
|
|
286
|
+
if (controlPanelPort) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
controlPanelPort = browser.runtime.connect({ name: CONTROL_PANEL_PORT_NAME });
|
|
290
|
+
controlPanelPort.onDisconnect.addListener(() => {
|
|
291
|
+
controlPanelPort = null;
|
|
292
|
+
if (keepAliveTimer) {
|
|
293
|
+
clearInterval(keepAliveTimer);
|
|
294
|
+
keepAliveTimer = null;
|
|
295
|
+
}
|
|
296
|
+
setActionStatus("Control panel connection was reset.", "error");
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
startControlPanelPort();
|
|
299
|
+
}, 1000);
|
|
300
|
+
});
|
|
301
|
+
keepAliveTimer = setInterval(() => {
|
|
302
|
+
try {
|
|
303
|
+
controlPanelPort?.postMessage({
|
|
304
|
+
type: "keepalive",
|
|
305
|
+
at: formatLocalTimestamp(new Date()),
|
|
306
|
+
});
|
|
307
|
+
} catch {
|
|
308
|
+
// ignore
|
|
309
|
+
}
|
|
310
|
+
}, 15000);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function waitForStorageResult(commandId, timeoutMs = 5000) {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const timer = setTimeout(() => {
|
|
316
|
+
browser.storage.onChanged.removeListener(listener);
|
|
317
|
+
reject(new Error("Request timed out."));
|
|
318
|
+
}, timeoutMs);
|
|
319
|
+
function listener(changes, areaName) {
|
|
320
|
+
if (areaName !== "local" || !changes[POPUP_COMMAND_RESULT_KEY]) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const nextValue = changes[POPUP_COMMAND_RESULT_KEY].newValue;
|
|
324
|
+
if (!nextValue || nextValue.command_id !== commandId) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
browser.storage.onChanged.removeListener(listener);
|
|
329
|
+
resolve(nextValue.result);
|
|
330
|
+
}
|
|
331
|
+
browser.storage.onChanged.addListener(listener);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function sendPopupCommand(type, payload = {}, timeoutMs = 5000) {
|
|
336
|
+
const commandId = `options-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
337
|
+
await storageSet({
|
|
338
|
+
[POPUP_COMMAND_KEY]: {
|
|
339
|
+
command_id: commandId,
|
|
340
|
+
type,
|
|
341
|
+
...payload,
|
|
342
|
+
at: formatLocalTimestamp(new Date()),
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
return await waitForStorageResult(commandId, timeoutMs);
|
|
347
|
+
} finally {
|
|
348
|
+
try {
|
|
349
|
+
await storageSet({
|
|
350
|
+
[POPUP_COMMAND_KEY]: null,
|
|
351
|
+
});
|
|
352
|
+
} catch {
|
|
353
|
+
// ignore
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function requestRecordingStatus() {
|
|
359
|
+
try {
|
|
360
|
+
return await sendPopupCommand("attach_recording_status");
|
|
361
|
+
} catch (error) {
|
|
362
|
+
return {
|
|
363
|
+
ok: false,
|
|
364
|
+
error: error instanceof Error ? error.message : String(error),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function refreshRecordingStatus() {
|
|
370
|
+
const result = await requestRecordingStatus();
|
|
371
|
+
if (!result?.ok) {
|
|
372
|
+
setActionStatus(result?.error || "Could not refresh recording status.", "error");
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function attachTab(tabId, options = {}) {
|
|
378
|
+
const { suppressSuccessStatus = false } = options;
|
|
379
|
+
setActionStatus("Attaching tab...", "neutral");
|
|
380
|
+
let result;
|
|
381
|
+
try {
|
|
382
|
+
result = await sendPopupCommand("attach_tab_selected", {
|
|
383
|
+
tab_id: tabId,
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
result = {
|
|
387
|
+
ok: false,
|
|
388
|
+
error: error instanceof Error ? error.message : String(error),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (!result?.ok) {
|
|
392
|
+
setActionStatus(result?.error || "Attach failed.", "error");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!suppressSuccessStatus) {
|
|
396
|
+
setActionStatus(`Attached tab ${result.tab?.tab_id}.`, "success");
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function startRecording() {
|
|
402
|
+
setActionStatus("Starting recording...", "neutral");
|
|
403
|
+
let result;
|
|
404
|
+
try {
|
|
405
|
+
result = await sendPopupCommand("attach_recording_start");
|
|
406
|
+
} catch (error) {
|
|
407
|
+
result = {
|
|
408
|
+
ok: false,
|
|
409
|
+
error: error instanceof Error ? error.message : String(error),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (!result?.ok) {
|
|
413
|
+
setActionStatus(result?.error || "Could not start recording.", "error");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
setActionStatus(
|
|
417
|
+
result?.recording?.bundle_dir_name
|
|
418
|
+
? `Recording started: ${result.recording.bundle_dir_name}`
|
|
419
|
+
: "Recording started.",
|
|
420
|
+
"success",
|
|
421
|
+
);
|
|
422
|
+
await refreshRecordingStatus();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function stopRecording() {
|
|
426
|
+
setActionStatus("Stopping recording...", "neutral");
|
|
427
|
+
let result;
|
|
428
|
+
try {
|
|
429
|
+
result = await sendPopupCommand("attach_recording_stop");
|
|
430
|
+
} catch (error) {
|
|
431
|
+
result = {
|
|
432
|
+
ok: false,
|
|
433
|
+
error: error instanceof Error ? error.message : String(error),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (!result?.ok) {
|
|
437
|
+
setActionStatus(result?.error || "Could not stop recording.", "error");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
setActionStatus("Recording stopped.", "success");
|
|
441
|
+
await refreshRecordingStatus();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function startRecordingForTab(tabId) {
|
|
445
|
+
const stored = await storageGet({
|
|
446
|
+
attach_connection_status: DEFAULT_CONNECTION_STATUS,
|
|
447
|
+
attach_recording_status: DEFAULT_RECORDING_STATUS,
|
|
448
|
+
attach_instance_id: DEFAULT_INSTANCE_ID,
|
|
449
|
+
});
|
|
450
|
+
const currentInstanceId = stored.attach_instance_id || stored.attach_connection_status?.instance_id;
|
|
451
|
+
const recordingStatus = stored.attach_recording_status ?? DEFAULT_RECORDING_STATUS;
|
|
452
|
+
if (
|
|
453
|
+
recordingStatus?.active === true &&
|
|
454
|
+
recordingStatus.recording?.instance_id &&
|
|
455
|
+
currentInstanceId &&
|
|
456
|
+
recordingStatus.recording.instance_id !== currentInstanceId
|
|
457
|
+
) {
|
|
458
|
+
setActionStatus("Recording is already active in another browser instance.", "error");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const attachResult = await attachTab(tabId, { suppressSuccessStatus: true });
|
|
462
|
+
if (!attachResult?.ok) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
await startRecording();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function injectSelectedFile() {
|
|
469
|
+
const fileInput = document.getElementById("inject-file");
|
|
470
|
+
const namespaceInput = document.getElementById("inject-namespace");
|
|
471
|
+
const file = fileInput?.files?.[0];
|
|
472
|
+
if (!file) {
|
|
473
|
+
setActionStatus("Choose a script file first.", "error");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const source = await file.text();
|
|
477
|
+
if (!source.trim()) {
|
|
478
|
+
setActionStatus("Selected script file is empty.", "error");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
setActionStatus(`Injecting ${file.name}...`, "neutral");
|
|
482
|
+
let result;
|
|
483
|
+
try {
|
|
484
|
+
result = await sendPopupCommand("attach_inject_script", {
|
|
485
|
+
source,
|
|
486
|
+
namespace: namespaceInput?.value?.trim() || "TELLY",
|
|
487
|
+
file_name: file.name,
|
|
488
|
+
});
|
|
489
|
+
} catch (error) {
|
|
490
|
+
result = {
|
|
491
|
+
ok: false,
|
|
492
|
+
error: error instanceof Error ? error.message : String(error),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (!result?.ok) {
|
|
496
|
+
setActionStatus(result?.error || "Script injection failed.", "error");
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
setActionStatus(`Injected ${file.name}.`, "success");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function setConnectionEnabled(enabled) {
|
|
503
|
+
await storageSet({
|
|
504
|
+
attach_connection_enabled: enabled,
|
|
505
|
+
});
|
|
506
|
+
setActionStatus(enabled ? "Connect requested." : "Disconnected.", "success");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function saveSettings(event) {
|
|
510
|
+
event.preventDefault();
|
|
511
|
+
const host = document.getElementById("host").value.trim() || DEFAULT_SETTINGS.host;
|
|
512
|
+
const portRaw = document.getElementById("port").value.trim();
|
|
513
|
+
const port = Number(portRaw || DEFAULT_SETTINGS.port);
|
|
514
|
+
|
|
515
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
516
|
+
setStatus("Port must be an integer between 1 and 65535.", true);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
await storageSet({
|
|
521
|
+
host,
|
|
522
|
+
port,
|
|
523
|
+
});
|
|
524
|
+
setStatus("Settings saved.");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function render() {
|
|
528
|
+
const [stored, tabs] = await Promise.all([
|
|
529
|
+
storageGet({
|
|
530
|
+
...DEFAULT_SETTINGS,
|
|
531
|
+
attach_connection_status: DEFAULT_CONNECTION_STATUS,
|
|
532
|
+
attach_connection_enabled: true,
|
|
533
|
+
[ATTACHED_TAB_KEY]: null,
|
|
534
|
+
attach_recording_status: DEFAULT_RECORDING_STATUS,
|
|
535
|
+
attach_instance_id: DEFAULT_INSTANCE_ID,
|
|
536
|
+
}),
|
|
537
|
+
queryTabs({ currentWindow: true }),
|
|
538
|
+
]);
|
|
539
|
+
|
|
540
|
+
document.getElementById("host").value = stored.host ?? DEFAULT_SETTINGS.host;
|
|
541
|
+
document.getElementById("port").value = String(stored.port ?? DEFAULT_SETTINGS.port);
|
|
542
|
+
const status = stored.attach_connection_status ?? DEFAULT_CONNECTION_STATUS;
|
|
543
|
+
const recordingStatus = stored.attach_recording_status ?? DEFAULT_RECORDING_STATUS;
|
|
544
|
+
const currentInstanceId = stored.attach_instance_id || status.instance_id;
|
|
545
|
+
|
|
546
|
+
setConnectionText(status.state, status.text || DEFAULT_CONNECTION_STATUS.text);
|
|
547
|
+
updateConnectionButtons(status, stored.attach_connection_enabled);
|
|
548
|
+
setMeta("endpoint", `Endpoint: ${stored.host}:${stored.port}`);
|
|
549
|
+
setMeta(
|
|
550
|
+
"session",
|
|
551
|
+
`Session: ${status.session_label || status.session_id || "-"}`,
|
|
552
|
+
);
|
|
553
|
+
setAttachedText(stored[ATTACHED_TAB_KEY]);
|
|
554
|
+
setRecordingText(recordingStatus);
|
|
555
|
+
setRecordingTabText(recordingStatus);
|
|
556
|
+
setRecordingScopeText(recordingStatus, currentInstanceId);
|
|
557
|
+
renderTabs(
|
|
558
|
+
tabs,
|
|
559
|
+
stored[ATTACHED_TAB_KEY]?.tab_id,
|
|
560
|
+
recordingStatus,
|
|
561
|
+
status.state === "connected",
|
|
562
|
+
currentInstanceId,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
document
|
|
567
|
+
.getElementById("settings-form")
|
|
568
|
+
.addEventListener("submit", (event) => {
|
|
569
|
+
void saveSettings(event);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
document.getElementById("connection-toggle-button").addEventListener("click", async () => {
|
|
573
|
+
const stored = await storageGet({
|
|
574
|
+
attach_connection_status: DEFAULT_CONNECTION_STATUS,
|
|
575
|
+
attach_connection_enabled: true,
|
|
576
|
+
});
|
|
577
|
+
const status = stored.attach_connection_status ?? DEFAULT_CONNECTION_STATUS;
|
|
578
|
+
const enabled = stored.attach_connection_enabled;
|
|
579
|
+
const connected = status.state === "connected" || status.state === "connecting";
|
|
580
|
+
await setConnectionEnabled(!(enabled !== false && connected));
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
browser.storage.onChanged.addListener((changes, areaName) => {
|
|
584
|
+
if (areaName !== "local") {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (
|
|
588
|
+
changes.attach_connection_status ||
|
|
589
|
+
changes.attach_instance_id ||
|
|
590
|
+
changes.host ||
|
|
591
|
+
changes.port ||
|
|
592
|
+
changes.attach_connection_enabled ||
|
|
593
|
+
changes.attach_recording_status ||
|
|
594
|
+
changes[ATTACHED_TAB_KEY]
|
|
595
|
+
) {
|
|
596
|
+
void render();
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
document.getElementById("inject-script-button").addEventListener("click", () => {
|
|
601
|
+
void injectSelectedFile();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
startControlPanelPort();
|
|
605
|
+
void render();
|
|
606
|
+
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 Chrome 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 Chrome 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>
|