@deadragdoll/tellymcp 0.0.11 → 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.
Files changed (62) hide show
  1. package/README-ru.md +48 -0
  2. package/README.md +47 -0
  3. package/TOOLS.md +232 -3
  4. package/VERSION.md +2 -2
  5. package/dist/cli.js +109 -1
  6. package/dist/services/features/telegram-mcp/browser.service.js +38 -1
  7. package/dist/services/features/telegram-mcp/gateway-socket.service.js +10 -0
  8. package/dist/services/features/telegram-mcp/mcp-server.service.js +12 -0
  9. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +14 -0
  10. package/dist/services/features/telegram-mcp/src/app/config/env.js +13 -0
  11. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +151 -2
  12. package/dist/services/features/telegram-mcp/src/features/browser/model/browserClickTool.js +1 -1
  13. package/dist/services/features/telegram-mcp/src/features/browser/model/browserDomTool.js +1 -1
  14. package/dist/services/features/telegram-mcp/src/features/browser/model/browserFillTool.js +1 -1
  15. package/dist/services/features/telegram-mcp/src/features/browser/model/browserInjectScriptTool.js +28 -0
  16. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListAttachedInstancesTool.js +33 -0
  17. package/dist/services/features/telegram-mcp/src/features/browser/model/browserListTabsTool.js +33 -0
  18. package/dist/services/features/telegram-mcp/src/features/browser/model/browserOpenTool.js +1 -1
  19. package/dist/services/features/telegram-mcp/src/features/browser/model/browserPressTool.js +1 -1
  20. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStartTool.js +28 -0
  21. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStatusTool.js +28 -0
  22. package/dist/services/features/telegram-mcp/src/features/browser/model/browserRecordingStopTool.js +28 -0
  23. package/dist/services/features/telegram-mcp/src/features/browser/model/browserScreenshotTool.js +1 -1
  24. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +543 -9
  25. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/browserRecordingBundle.js +502 -0
  26. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachRegistry.js +79 -0
  27. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/firefoxAttachServer.js +559 -0
  28. package/dist/services/features/telegram-mcp/src/features/browser-attach/model/types.js +2 -0
  29. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +48 -0
  30. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/en.js +5 -1
  31. package/dist/services/features/telegram-mcp/src/shared/i18n/resources/ru.js +5 -1
  32. package/dist/services/features/telegram-mcp/src/shared/integrations/redis/stateStore.js +56 -0
  33. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +37 -0
  34. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConsoleRegistry.js +13 -7
  35. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConstructorWiring.js +9 -0
  36. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMenuShell.js +3 -0
  37. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportPayloadState.js +7 -0
  38. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalActions.js +231 -34
  39. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportTerminalRuntime.js +61 -6
  40. package/dist/services/features/telegram-mcp/src/shared/lib/terminalPromptDetection.js +200 -28
  41. package/docs/STANDALONE-ru.md +42 -6
  42. package/docs/STANDALONE.md +42 -6
  43. package/package.json +6 -3
  44. package/packages/chrome-attach-extension/dist/background.js +1326 -0
  45. package/packages/chrome-attach-extension/dist/icon.svg +6 -0
  46. package/packages/chrome-attach-extension/dist/manifest.json +36 -0
  47. package/packages/chrome-attach-extension/dist/options.html +312 -0
  48. package/packages/chrome-attach-extension/dist/options.js +593 -0
  49. package/packages/chrome-attach-extension/dist/popup.html +93 -0
  50. package/packages/chrome-attach-extension/dist/popup.js +79 -0
  51. package/packages/chrome-attach-extension/dist/recorder-content.js +83 -0
  52. package/packages/chrome-attach-extension/dist/recorder-page.js +266 -0
  53. package/packages/firefox-attach-extension/README.md +13 -0
  54. package/packages/firefox-attach-extension/dist/background.js +1242 -0
  55. package/packages/firefox-attach-extension/dist/icon.svg +6 -0
  56. package/packages/firefox-attach-extension/dist/manifest.json +56 -0
  57. package/packages/firefox-attach-extension/dist/options.html +312 -0
  58. package/packages/firefox-attach-extension/dist/options.js +527 -0
  59. package/packages/firefox-attach-extension/dist/popup.html +93 -0
  60. package/packages/firefox-attach-extension/dist/popup.js +64 -0
  61. package/packages/firefox-attach-extension/dist/recorder-content.js +77 -0
  62. package/packages/firefox-attach-extension/dist/recorder-page.js +302 -0
@@ -25,9 +25,35 @@ class TransportTerminalRuntime {
25
25
  }
26
26
  startPromptScan() {
27
27
  if (!this.host.config.terminal.promptScanEnabled) {
28
+ this.host.logger.debug("terminal prompt scan disabled", {
29
+ promptScanEnabled: this.host.config.terminal.promptScanEnabled,
30
+ });
31
+ return;
32
+ }
33
+ if (this.host.config.distributed.mode === "gateway") {
34
+ this.host.logger.info("terminal prompt scan armed for live clients", {
35
+ intervalSeconds: this.host.config.terminal.promptScanIntervalSeconds,
36
+ cooldownSeconds: this.host.config.terminal.promptScanCooldownSeconds,
37
+ strategy: this.host.config.terminal.promptScanStrategy,
38
+ minScore: this.host.config.terminal.promptScanMinScore,
39
+ });
40
+ return;
41
+ }
42
+ this.ensurePromptScanRunning();
43
+ }
44
+ clearTerminalPromptScanTimer() {
45
+ if (this.terminalPromptScanTimer) {
46
+ clearInterval(this.terminalPromptScanTimer);
47
+ this.terminalPromptScanTimer = undefined;
48
+ }
49
+ }
50
+ ensurePromptScanRunning() {
51
+ if (!this.host.config.terminal.promptScanEnabled) {
52
+ return;
53
+ }
54
+ if (this.terminalPromptScanTimer) {
28
55
  return;
29
56
  }
30
- this.clearTerminalPromptScanTimer();
31
57
  const intervalMs = this.host.config.terminal.promptScanIntervalSeconds * 1000;
32
58
  const timer = setInterval(() => {
33
59
  void this.runTerminalPromptScanCycle().catch((error) => {
@@ -54,11 +80,12 @@ class TransportTerminalRuntime {
54
80
  });
55
81
  });
56
82
  }
57
- clearTerminalPromptScanTimer() {
58
- if (this.terminalPromptScanTimer) {
59
- clearInterval(this.terminalPromptScanTimer);
60
- this.terminalPromptScanTimer = undefined;
83
+ pausePromptScan() {
84
+ if (!this.terminalPromptScanTimer) {
85
+ return;
61
86
  }
87
+ this.clearTerminalPromptScanTimer();
88
+ this.host.logger.info("terminal prompt scan paused", {});
62
89
  }
63
90
  scheduleTerminalNudgeForInboxMessage(sessionId, session) {
64
91
  if (!this.host.config.terminal.nudgeEnabled) {
@@ -153,15 +180,43 @@ class TransportTerminalRuntime {
153
180
  }
154
181
  }
155
182
  async runTerminalPromptScanCycle() {
156
- if (!this.host.config.terminal.promptScanEnabled || this.terminalPromptScanInFlight) {
183
+ if (!this.host.config.terminal.promptScanEnabled) {
184
+ this.host.logger.debug("terminal prompt scan cycle skipped", {
185
+ skipReason: "disabled",
186
+ });
187
+ return;
188
+ }
189
+ if (this.terminalPromptScanInFlight) {
190
+ this.host.logger.debug("terminal prompt scan cycle skipped", {
191
+ skipReason: "in_flight",
192
+ });
157
193
  return;
158
194
  }
159
195
  this.terminalPromptScanInFlight = true;
160
196
  try {
197
+ if (this.host.ensureGatewayScopedConsolesBoundForPrincipal) {
198
+ const principals = await this.host.bindingStore.listBoundPrincipals();
199
+ this.host.logger.debug("terminal prompt scan syncing bound gateway principals", {
200
+ principalCount: principals.length,
201
+ });
202
+ for (const principal of principals) {
203
+ await this.host.ensureGatewayScopedConsolesBoundForPrincipal(principal);
204
+ }
205
+ }
161
206
  const sessions = await this.host.sessionStore.listSessions();
207
+ this.host.logger.debug("terminal prompt scan cycle started", {
208
+ sessionCount: sessions.length,
209
+ });
210
+ if (sessions.length === 0) {
211
+ this.host.logger.debug("terminal prompt scan cycle found no sessions");
212
+ return;
213
+ }
162
214
  for (const session of sessions) {
163
215
  await this.actions.scanPromptForSession(session);
164
216
  }
217
+ this.host.logger.debug("terminal prompt scan cycle completed", {
218
+ sessionCount: sessions.length,
219
+ });
165
220
  }
166
221
  finally {
167
222
  this.terminalPromptScanInFlight = false;
@@ -16,6 +16,11 @@ const STRONG_PATTERNS = [
16
16
  score: 5,
17
17
  reason: "press_enter_prompt",
18
18
  },
19
+ {
20
+ pattern: /press\s+enter\s+to\s+confirm\b.*\besc\s+to\s+cancel/iu,
21
+ score: 5,
22
+ reason: "confirm_cancel_prompt",
23
+ },
19
24
  {
20
25
  pattern: /waiting\s+for\s+(?:user\s+)?input|awaiting\s+(?:user\s+)?(?:input|confirmation)|requires?\s+approval|need\s+your\s+(?:input|permission|approval)|cannot\s+continue\s+without/iu,
21
26
  score: 4,
@@ -46,6 +51,11 @@ const STRONG_PATTERNS = [
46
51
  score: 4,
47
52
  reason: "tool_continue_prompt",
48
53
  },
54
+ {
55
+ pattern: /\bwould\s+you\s+like\s+to\s+run\s+the\s+following\s+command\b/iu,
56
+ score: 5,
57
+ reason: "command_approval_prompt",
58
+ },
49
59
  ];
50
60
  const MEDIUM_PATTERNS = [
51
61
  {
@@ -64,6 +74,17 @@ const MEDIUM_PATTERNS = [
64
74
  reason: "tool_choice_keyword",
65
75
  },
66
76
  ];
77
+ const NUMBERED_CHOICE_LINE_PATTERN = /^(?:[>›*•-]\s*)?\d+\.\s+.+$/u;
78
+ const CONFIRM_CANCEL_FOOTER_PATTERN = /\b(?:press\s+enter\s+to\s+confirm|enter\s+to\s+submit)\b.*\besc\s+to\s+cancel\b/iu;
79
+ const SUBMIT_CANCEL_FOOTER_PATTERN = /\benter\s+to\s+submit\b.*\besc\s+to\s+cancel\b/iu;
80
+ const CONTINUE_FOOTER_PATTERN = /\b(?:press\s+enter\s+to\s+continue|hit\s+enter\s+to\s+continue)\b/iu;
81
+ const NUMBERED_HOTKEY_FOOTER_PATTERN = /\bpress\s+(?:(?:\d+\s*,\s*)+\s*(?:or\s+)?\d+\s*,?\s*)?(?:enter\b.*\besc|esc\b.*\benter|\d+\b.*\benter|\d+\b.*\besc).*\b(?:quit|continue|cancel|confirm|submit)\b/iu;
82
+ const POSITIVE_CHOICE_PATTERN = /\b(?:yes|allow|approve|proceed|continue)\b/iu;
83
+ const NEGATIVE_CHOICE_PATTERN = /\b(?:no|cancel|deny|reject)\b/iu;
84
+ const STICKY_CHOICE_PATTERN = /\bdon'?t\s+ask\s+again|always\s+allow|for\s+this\s+session\b/iu;
85
+ const SKIP_CHOICE_PATTERN = /\bskip\b/iu;
86
+ const LEADING_SELECTION_MARKER_PATTERN = /^(?:[>›*•-]\s*)(?=\d+\.\s)/u;
87
+ const ACTION_HINT_PATTERN = /\b(?:press|input|choose|choice|select|option|enter|esc(?:ape)?|accept|decline|confirm|cancel|continue|proceed|yes|no)\b|(?:\[[Yy](?:es)?\/[Nn](?:o)?\])|(?:\([Yy](?:es)?\/[Nn](?:o)?\))/u;
67
88
  function normalizeLine(line) {
68
89
  return line
69
90
  .replaceAll("\r", "")
@@ -119,25 +140,6 @@ function scoreMediumLine(line, strategy) {
119
140
  function isStrongReason(reason) {
120
141
  return STRONG_PATTERNS.some((candidate) => candidate.reason === reason);
121
142
  }
122
- function detectGroupedSignals(candidateLines, strategy) {
123
- const signals = [];
124
- const hasPrimaryAllowChoice = candidateLines.some((line) => /^>?[\s\d.]*allow\b/iu.test(line));
125
- const hasSecondaryApprovalChoice = candidateLines.some((line) => /\b(?:allow\s+once|allow\s+for\s+this\s+session|always\s+allow|cancel)\b/iu.test(line));
126
- const hasSubmitCancelHint = candidateLines.some((line) => /\benter\s+to\s+submit\b.*\besc\s+to\s+cancel\b/iu.test(line));
127
- if (hasPrimaryAllowChoice && hasSecondaryApprovalChoice) {
128
- signals.push({
129
- score: 5,
130
- reason: "approval_choice_group",
131
- });
132
- }
133
- if (hasSubmitCancelHint) {
134
- signals.push({
135
- score: strategy === "balanced" ? 2 : 3,
136
- reason: "submit_cancel_hint",
137
- });
138
- }
139
- return signals;
140
- }
141
143
  function collectCandidateLines(rawText, maxLines) {
142
144
  const normalized = rawText
143
145
  .split("\n")
@@ -145,6 +147,168 @@ function collectCandidateLines(rawText, maxLines) {
145
147
  .filter((line) => line.length > 0);
146
148
  return normalized.slice(-Math.max(1, maxLines));
147
149
  }
150
+ function normalizeFingerprintLine(line) {
151
+ return normalizeLine(line)
152
+ .replace(LEADING_SELECTION_MARKER_PATTERN, "")
153
+ .replace(/\s+/gu, " ")
154
+ .trim();
155
+ }
156
+ function buildDetectionFingerprint(strategy, matchedLines) {
157
+ const normalizedExcerpt = matchedLines
158
+ .map(normalizeFingerprintLine)
159
+ .join("\n");
160
+ return (0, node_crypto_1.createHash)("sha1")
161
+ .update(`${strategy}\n${normalizedExcerpt}`)
162
+ .digest("hex");
163
+ }
164
+ function buildExcerptWithContext(candidateLines, matchedLines) {
165
+ if (matchedLines.length === 0) {
166
+ return "";
167
+ }
168
+ const firstMatchedIndex = candidateLines.findIndex((line) => line === matchedLines[0]);
169
+ const lastMatchedIndex = matchedLines.length === 1
170
+ ? firstMatchedIndex
171
+ : candidateLines.findIndex((line, index) => index >= Math.max(0, firstMatchedIndex) &&
172
+ line === matchedLines[matchedLines.length - 1]);
173
+ if (firstMatchedIndex < 0 || lastMatchedIndex < 0) {
174
+ return matchedLines.slice(-8).join("\n");
175
+ }
176
+ const excerptStart = Math.max(0, firstMatchedIndex - 2);
177
+ const excerptEnd = Math.min(candidateLines.length, lastMatchedIndex + 1);
178
+ return candidateLines.slice(excerptStart, excerptEnd).slice(-8).join("\n");
179
+ }
180
+ function findChoiceActionBlock(candidateLines) {
181
+ for (let startIndex = candidateLines.length - 1; startIndex >= 0; startIndex -= 1) {
182
+ const line = candidateLines[startIndex] ?? "";
183
+ if (!NUMBERED_CHOICE_LINE_PATTERN.test(line)) {
184
+ continue;
185
+ }
186
+ const choiceLines = [];
187
+ let firstChoiceIndex = startIndex;
188
+ while (firstChoiceIndex >= 0) {
189
+ const currentLine = candidateLines[firstChoiceIndex] ?? "";
190
+ if (!NUMBERED_CHOICE_LINE_PATTERN.test(currentLine)) {
191
+ break;
192
+ }
193
+ choiceLines.unshift(currentLine);
194
+ firstChoiceIndex -= 1;
195
+ }
196
+ firstChoiceIndex += 1;
197
+ if (choiceLines.length < 2) {
198
+ continue;
199
+ }
200
+ const lastChoiceIndex = firstChoiceIndex + choiceLines.length - 1;
201
+ const headerLines = candidateLines.slice(Math.max(0, firstChoiceIndex - 5), firstChoiceIndex);
202
+ const tailLines = candidateLines.slice(lastChoiceIndex + 1, Math.min(candidateLines.length, lastChoiceIndex + 4));
203
+ const footerLine = tailLines[0] ?? null;
204
+ const nearbyLines = [...headerLines, ...tailLines];
205
+ const hasActionHint = nearbyLines.some((candidate) => ACTION_HINT_PATTERN.test(candidate));
206
+ const hasChoiceSemantics = POSITIVE_CHOICE_PATTERN.test(choiceLines.join("\n")) ||
207
+ NEGATIVE_CHOICE_PATTERN.test(choiceLines.join("\n")) ||
208
+ STICKY_CHOICE_PATTERN.test(choiceLines.join("\n")) ||
209
+ SKIP_CHOICE_PATTERN.test(choiceLines.join("\n"));
210
+ if (!hasActionHint && !hasChoiceSemantics) {
211
+ continue;
212
+ }
213
+ return {
214
+ footerLine,
215
+ choiceLines,
216
+ headerLines,
217
+ tailLines,
218
+ };
219
+ }
220
+ return null;
221
+ }
222
+ function detectChoiceFooterSignals(candidateLines, strategy) {
223
+ const block = findChoiceActionBlock(candidateLines);
224
+ if (!block) {
225
+ return null;
226
+ }
227
+ const choiceText = block.choiceLines.join("\n");
228
+ const contextLines = [
229
+ ...block.headerLines,
230
+ ...block.choiceLines,
231
+ ...(block.footerLine ? [block.footerLine] : []),
232
+ ...block.tailLines,
233
+ ];
234
+ const reasons = new Set(["numbered_choice_group"]);
235
+ const matchedLines = [
236
+ ...block.choiceLines,
237
+ ...(block.footerLine ? [block.footerLine] : []),
238
+ ...block.tailLines,
239
+ ];
240
+ let score = strategy === "balanced" ? 5 : 6;
241
+ const footerLine = block.footerLine ?? "";
242
+ if (CONFIRM_CANCEL_FOOTER_PATTERN.test(footerLine)) {
243
+ reasons.add("confirm_cancel_footer");
244
+ score += strategy === "balanced" ? 2 : 3;
245
+ }
246
+ if (CONTINUE_FOOTER_PATTERN.test(footerLine)) {
247
+ reasons.add("continue_footer");
248
+ score += strategy === "balanced" ? 2 : 3;
249
+ }
250
+ if (NUMBERED_HOTKEY_FOOTER_PATTERN.test(footerLine)) {
251
+ reasons.add("numbered_hotkey_footer");
252
+ score += strategy === "balanced" ? 2 : 3;
253
+ }
254
+ if (SUBMIT_CANCEL_FOOTER_PATTERN.test(footerLine)) {
255
+ reasons.add("submit_cancel_hint");
256
+ }
257
+ if (/press\s+enter\s+to\s+confirm\b/iu.test(footerLine)) {
258
+ reasons.add("confirm_cancel_hint");
259
+ }
260
+ if (contextLines.some((line) => ACTION_HINT_PATTERN.test(line))) {
261
+ reasons.add("action_hint_present");
262
+ score += 2;
263
+ }
264
+ if (POSITIVE_CHOICE_PATTERN.test(choiceText)) {
265
+ reasons.add("positive_choice_present");
266
+ score += 2;
267
+ }
268
+ if (NEGATIVE_CHOICE_PATTERN.test(choiceText)) {
269
+ reasons.add("negative_choice_present");
270
+ score += 2;
271
+ }
272
+ if (STICKY_CHOICE_PATTERN.test(choiceText)) {
273
+ reasons.add("sticky_choice_present");
274
+ score += 2;
275
+ }
276
+ if (SKIP_CHOICE_PATTERN.test(choiceText)) {
277
+ reasons.add("skip_choice_present");
278
+ score += 2;
279
+ }
280
+ if (reasons.has("positive_choice_present") && reasons.has("negative_choice_present")) {
281
+ reasons.add("yes_no_choice_group");
282
+ reasons.add("approval_choice_group");
283
+ score += 3;
284
+ }
285
+ if (reasons.has("positive_choice_present") && reasons.has("skip_choice_present")) {
286
+ reasons.add("choice_update_group");
287
+ score += 2;
288
+ }
289
+ if (/^(?:[>›*•-]\s*)?(?:\d+\.\s+)?yes,\s+proceed\b/imu.test(choiceText) &&
290
+ reasons.has("sticky_choice_present")) {
291
+ reasons.add("proceed_choice_group");
292
+ }
293
+ for (const line of contextLines) {
294
+ for (const contribution of scoreStrongLine(line)) {
295
+ reasons.add(contribution.reason);
296
+ score += contribution.score;
297
+ }
298
+ }
299
+ return {
300
+ score,
301
+ reasons: Array.from(reasons),
302
+ matchedLines,
303
+ promptActions: {
304
+ numberedOptions: block.choiceLines
305
+ .map((line) => line.match(/^(?:[>›*•-]\s*)?(\d+)\.\s+/u)?.[1] ?? null)
306
+ .filter((value) => Boolean(value)),
307
+ hasEnter: contextLines.some((line) => /\benter\b/iu.test(line)),
308
+ hasEscape: contextLines.some((line) => /\besc(?:ape)?\b/iu.test(line)),
309
+ },
310
+ };
311
+ }
148
312
  function detectTerminalInteractivePrompt(rawText, options = {}) {
149
313
  const strategy = options.strategy ?? "strict";
150
314
  const minScore = options.minScore ?? (strategy === "balanced" ? 4 : 5);
@@ -152,6 +316,20 @@ function detectTerminalInteractivePrompt(rawText, options = {}) {
152
316
  if (candidateLines.length === 0) {
153
317
  return null;
154
318
  }
319
+ const choiceFooterDetection = detectChoiceFooterSignals(candidateLines, strategy);
320
+ if (choiceFooterDetection && choiceFooterDetection.score >= minScore) {
321
+ const matchedLines = choiceFooterDetection.matchedLines.slice(-6);
322
+ const excerpt = buildExcerptWithContext(candidateLines, matchedLines);
323
+ const fingerprint = buildDetectionFingerprint(strategy, matchedLines);
324
+ return {
325
+ score: choiceFooterDetection.score,
326
+ fingerprint,
327
+ excerpt,
328
+ matchedLines,
329
+ reasons: choiceFooterDetection.reasons,
330
+ promptActions: choiceFooterDetection.promptActions,
331
+ };
332
+ }
155
333
  const scoredLines = [];
156
334
  let score = 0;
157
335
  const reasons = new Set();
@@ -174,12 +352,8 @@ function detectTerminalInteractivePrompt(rawText, options = {}) {
174
352
  score += 3;
175
353
  reasons.add("multiple_options");
176
354
  }
177
- for (const signal of detectGroupedSignals(candidateLines, strategy)) {
178
- score += signal.score;
179
- reasons.add(signal.reason);
180
- }
181
355
  const hasStrongReason = Array.from(reasons).some(isStrongReason);
182
- const hasGroupedReason = reasons.has("approval_choice_group") || reasons.has("submit_cancel_hint");
356
+ const hasGroupedReason = reasons.has("multiple_options");
183
357
  const hasOnlyQuestionMarkReason = reasons.size === 1 && reasons.has("question_mark");
184
358
  if (strategy === "strict") {
185
359
  if (hasOnlyQuestionMarkReason) {
@@ -223,10 +397,8 @@ function detectTerminalInteractivePrompt(rawText, options = {}) {
223
397
  const matchedLines = scoredLines
224
398
  .map((entry) => entry.line)
225
399
  .slice(-6);
226
- const excerpt = matchedLines.join("\n");
227
- const fingerprint = (0, node_crypto_1.createHash)("sha1")
228
- .update(`${strategy}\n${excerpt}`)
229
- .digest("hex");
400
+ const excerpt = buildExcerptWithContext(candidateLines, matchedLines);
401
+ const fingerprint = buildDetectionFingerprint(strategy, matchedLines);
230
402
  return {
231
403
  score,
232
404
  fingerprint,
@@ -31,6 +31,8 @@ npm install -g @deadragdoll/tellymcp
31
31
 
32
32
  ```bash
33
33
  tellymcp browser install
34
+ tellymcp extension firefox
35
+ tellymcp extension chrome
34
36
  tellymcp codex-plugin install
35
37
  ```
36
38
 
@@ -91,7 +93,41 @@ TELLYMCP_SESSION_ID=NEW
91
93
  TELLYMCP_SESSION_LABEL=NEW
92
94
  ```
93
95
 
94
- ## 5. Запуск
96
+ ## 5. Attach extensions для браузера
97
+
98
+ Если нужно attach'иться к уже открытому Firefox или Chrome на агентской машине, включи локальный attach bridge в env этого агента:
99
+
100
+ ```env
101
+ BROWSER_ATTACH_ENABLED=true
102
+ BROWSER_ATTACH_WS_HOST=127.0.0.1
103
+ BROWSER_ATTACH_WS_PORT=9999
104
+ BROWSER_ATTACH_WS_PATH=/browser-attach/ws
105
+ ```
106
+
107
+ Выгрузи unpacked extension bundle из установленного пакета:
108
+
109
+ ```bash
110
+ tellymcp extension firefox
111
+ tellymcp extension chrome
112
+ ```
113
+
114
+ Команда создаст один из каталогов:
115
+
116
+ - `./tellymcp-firefox-attach`
117
+ - `./tellymcp-chrome-attach`
118
+
119
+ Дальше загрузи его в локальный браузер:
120
+
121
+ - Firefox: `about:debugging#/runtime/this-firefox` -> `Load Temporary Add-on` -> выбрать `manifest.json`
122
+ - Chrome: `chrome://extensions` -> включить Developer mode -> `Load unpacked` -> выбрать выгруженный каталог
123
+
124
+ После этого browser control panel умеет:
125
+
126
+ - attach'ить текущую agent-сессию к живой вкладке
127
+ - запускать и останавливать structured recording bundles в `.mcp-xchange/web/...`
128
+ - инжектить helper scripts в attached tab
129
+
130
+ ## 6. Запуск
95
131
 
96
132
  Gateway:
97
133
 
@@ -111,7 +147,7 @@ tellymcp run --env .env -s NEW
111
147
  tellymcp run
112
148
  ```
113
149
 
114
- ## 6. Webhook
150
+ ## 7. Webhook
115
151
 
116
152
  Gateway поддерживает polling и webhook.
117
153
 
@@ -131,7 +167,7 @@ TELEGRAM_WEBHOOK_SECRET=change_me_webhook_secret
131
167
  - `/api/webapp`
132
168
  - `/api/healthz`
133
169
 
134
- ## 7. MCP
170
+ ## 8. MCP
135
171
 
136
172
  Локальный client-mode MCP endpoint:
137
173
 
@@ -141,7 +177,7 @@ http://127.0.0.1:8787/mcp
141
177
 
142
178
  Используй MCP HTTP endpoint, который поднимает `tellymcp run`.
143
179
 
144
- ## 8. Проверки
180
+ ## 9. Проверки
145
181
 
146
182
  ```bash
147
183
  tellymcp doctor --env .env
@@ -153,7 +189,7 @@ tellymcp doctor --env .env
153
189
  tellymcp system-prune --env .env --yes
154
190
  ```
155
191
 
156
- ## 9. Операционные заметки
192
+ ## 10. Операционные заметки
157
193
 
158
194
  - gateway-бот — это основной user-facing control plane
159
195
  - консоли видны из gateway live registry
@@ -162,7 +198,7 @@ tellymcp system-prune --env .env --yes
162
198
  - browser screenshot для человека лучше отправлять через `browser_screenshot(send_to_telegram=true)`
163
199
  - файлы между консолями лучше отправлять через `send_partner_file`
164
200
 
165
- ## 10. Чего не делать в новых setup
201
+ ## 11. Чего не делать в новых setup
166
202
 
167
203
  Не строй новые setup вокруг:
168
204
 
@@ -31,6 +31,8 @@ Optional:
31
31
 
32
32
  ```bash
33
33
  tellymcp browser install
34
+ tellymcp extension firefox
35
+ tellymcp extension chrome
34
36
  tellymcp codex-plugin install
35
37
  ```
36
38
 
@@ -91,7 +93,41 @@ TELLYMCP_SESSION_ID=NEW
91
93
  TELLYMCP_SESSION_LABEL=NEW
92
94
  ```
93
95
 
94
- ## 5. Run
96
+ ## 5. Attached Browser Extensions
97
+
98
+ If you want to attach TellyMCP to an already running Firefox or Chrome tab on an agent machine, enable the local attach bridge in that agent env:
99
+
100
+ ```env
101
+ BROWSER_ATTACH_ENABLED=true
102
+ BROWSER_ATTACH_WS_HOST=127.0.0.1
103
+ BROWSER_ATTACH_WS_PORT=9999
104
+ BROWSER_ATTACH_WS_PATH=/browser-attach/ws
105
+ ```
106
+
107
+ Export the unpacked extension bundle from the installed package:
108
+
109
+ ```bash
110
+ tellymcp extension firefox
111
+ tellymcp extension chrome
112
+ ```
113
+
114
+ This creates one of:
115
+
116
+ - `./tellymcp-firefox-attach`
117
+ - `./tellymcp-chrome-attach`
118
+
119
+ Load it into the local browser:
120
+
121
+ - Firefox: `about:debugging#/runtime/this-firefox` -> `Load Temporary Add-on` -> choose `manifest.json`
122
+ - Chrome: `chrome://extensions` -> enable Developer mode -> `Load unpacked` -> choose the exported directory
123
+
124
+ After that the browser control panel can:
125
+
126
+ - attach the current agent session to a live browser tab
127
+ - start and stop structured recording bundles in `.mcp-xchange/web/...`
128
+ - inject helper scripts into the attached tab
129
+
130
+ ## 6. Run
95
131
 
96
132
  Gateway:
97
133
 
@@ -111,7 +147,7 @@ After `.mcpsession.json` is created in the workspace, later runs can usually use
111
147
  tellymcp run
112
148
  ```
113
149
 
114
- ## 6. Webhook
150
+ ## 7. Webhook
115
151
 
116
152
  Gateway supports polling and webhook.
117
153
 
@@ -131,7 +167,7 @@ If nginx already proxies `location /api/ { ... }` to the standalone listener, th
131
167
  - `/api/webapp`
132
168
  - `/api/healthz`
133
169
 
134
- ## 7. MCP
170
+ ## 8. MCP
135
171
 
136
172
  Local client-mode MCP:
137
173
 
@@ -141,7 +177,7 @@ http://127.0.0.1:8787/mcp
141
177
 
142
178
  Use the MCP HTTP endpoint exposed by `tellymcp run`.
143
179
 
144
- ## 8. Health Checks
180
+ ## 9. Health Checks
145
181
 
146
182
  ```bash
147
183
  tellymcp doctor --env .env
@@ -153,7 +189,7 @@ Destructive cleanup:
153
189
  tellymcp system-prune --env .env --yes
154
190
  ```
155
191
 
156
- ## 9. Operational Notes
192
+ ## 10. Operational Notes
157
193
 
158
194
  - the gateway bot is the user-facing control plane
159
195
  - consoles are discovered from the gateway live registry
@@ -162,7 +198,7 @@ tellymcp system-prune --env .env --yes
162
198
  - browser screenshot replies to humans should use `browser_screenshot(send_to_telegram=true)`
163
199
  - file results between consoles should use `send_partner_file`
164
200
 
165
- ## 10. Legacy Concepts To Avoid
201
+ ## 11. Legacy Concepts To Avoid
166
202
 
167
203
  Do not build new setups around:
168
204
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deadragdoll/tellymcp",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "TellyMCP - Telegram control plane for MCP-connected coding agents",
5
5
  "main": "dist/services/features/telegram-mcp/runtime.service.js",
6
6
  "bin": {
@@ -8,6 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
+ "packages/firefox-attach-extension/dist",
12
+ "packages/chrome-attach-extension/dist",
11
13
  "scripts",
12
14
  "config/templates",
13
15
  "config/codex",
@@ -36,8 +38,9 @@
36
38
  },
37
39
  "scripts": {
38
40
  "postinstall": "node ./scripts/postinstall.js",
39
- "prepack": "yarn build",
40
- "prepublishOnly": "yarn build && yarn lint && yarn test",
41
+ "build:extensions": "node ./packages/firefox-attach-extension/scripts/build.mjs && node ./packages/chrome-attach-extension/scripts/build.mjs",
42
+ "prepack": "yarn build && yarn build:extensions",
43
+ "prepublishOnly": "yarn build && yarn build:extensions && yarn lint && yarn test",
41
44
  "mcp:local": "codex mcp add telegramHuman --url http://127.0.0.1:8787/mcp",
42
45
  "dev:gw": "TS_NODE_TRANSPILE_ONLY=1 TS_NODE_EXPERIMENTAL_RESOLVER=1 NODE_OPTIONS=--openssl-legacy-provider ts-node ./node_modules/moleculer/bin/moleculer-runner.js src/services --config src/moleculer.config.ts --envfile /opt/tellymcp-gateway/.env-gateway --hot --mask \"**/*.service.ts\"",
43
46
  "dev:builder": "TS_NODE_TRANSPILE_ONLY=1 TS_NODE_EXPERIMENTAL_RESOLVER=1 NODE_OPTIONS=--openssl-legacy-provider ts-node ./node_modules/moleculer/bin/moleculer-runner.js src/services --config src/moleculer.config.ts --envfile ./.env-builder --hot --mask \"**/*.service.ts\"",