Skip to content

Commit 0cbe66e

Browse files
committed
chore: release v0.9.18 — support Chrome 146 consent-based remote debugging
Chrome 146 opens a random port (e.g. 63924) via chrome://inspect/#remote-debugging. HTTP endpoints return 404; WebSocket to /devtools/browser/{uuid} shows a consent dialog, and connects after user clicks Allow. - _isCdpPortAlive(): TCP probe fallback to detect Chrome 146 consent port - _isChrome146ConsentPort flag: skip HTTP discovery when true - _discoverTargetViaConsentPort(): WebSocket browser-level connect + Target.getTargets to list/create tabs (waits 30s for user to click Allow) - _generateUuid(): RFC 4122 v4 UUID for browser-level WS path
1 parent ca2ded8 commit 0cbe66e

18 files changed

Lines changed: 190 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 0.9.18
2+
3+
**Support Chrome 146 consent-based remote debugging port**
4+
5+
### Changes
6+
- TODO: Add your changes here
7+
8+
---
9+
110
## 0.9.17
211

312
**Session-copy profile: preserve user logins when Chrome 145+ blocks default profile debug port**

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ Then batch multiple actions in one call:
440440

441441
```yaml
442442
dependencies:
443-
flutter_skill: ^0.9.17
443+
flutter_skill: ^0.9.18
444444
```
445445
446446
```dart

intellij-plugin/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = "com.aidashboad"
8-
version = "0.9.17"
8+
version = "0.9.18"
99

1010
repositories {
1111
mavenCentral()

intellij-plugin/src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<idea-plugin>
22
<id>com.aidashboad.flutterskill</id>
33
<name>Flutter Skill - AI App Automation</name>
4-
<version>0.9.17</version>
4+
<version>0.9.18</version>
55
<vendor email="support@ai-dashboad.com" url="https://github.com/ai-dashboad/flutter-skill">ai-dashboad</vendor>
66

77
<description><![CDATA[

lib/src/bridge/cdp_driver.dart

Lines changed: 160 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ library;
33
import 'dart:async';
44
import 'dart:convert';
55
import 'dart:io';
6+
import 'dart:math';
67

78
import 'package:http/http.dart' as http;
89

@@ -44,6 +45,7 @@ class CdpDriver implements AppDriver {
4445
final Map<String, List<void Function(Map<String, dynamic>)>> _eventListeners =
4546
{};
4647
bool _dialogHandlerInstalled = false;
48+
bool _isChrome146ConsentPort = false;
4749
final Map<String, Map<String, dynamic>> _interceptRules = {};
4850

4951
/// Create a CDP driver.
@@ -226,8 +228,11 @@ class CdpDriver implements AppDriver {
226228
}
227229
}
228230

229-
/// Check if CDP port is already responding
231+
/// Check if CDP port is already responding.
232+
/// Handles both standard CDP (HTTP /json/version) and Chrome 146+'s
233+
/// consent-based port (WebSocket /devtools/browser/{uuid} — no HTTP).
230234
Future<bool> _isCdpPortAlive() async {
235+
// Standard CDP: HTTP /json/version
231236
try {
232237
final client = HttpClient()
233238
..connectionTimeout = const Duration(seconds: 2);
@@ -236,10 +241,27 @@ class CdpDriver implements AppDriver {
236241
final response = await request.close();
237242
await response.drain<void>();
238243
client.close();
244+
_isChrome146ConsentPort = false;
239245
return true;
240-
} catch (_) {
241-
return false;
242-
}
246+
} catch (_) {}
247+
248+
// Chrome 146+ consent port: HTTP returns 404, but WebSocket to
249+
// /devtools/browser/{uuid} either connects or hangs (waiting for Allow).
250+
// A quick 1s probe: if the socket connects (not refused), port is alive.
251+
try {
252+
final sock = await Socket.connect(
253+
InternetAddress.loopbackIPv4,
254+
_port,
255+
timeout: const Duration(seconds: 1),
256+
);
257+
await sock.close();
258+
// Port is open. Check if it's a Chrome 146 consent port by probing /json/version.
259+
// 404 = Chrome 146 consent port. Connection refused = not alive.
260+
_isChrome146ConsentPort = true;
261+
return true;
262+
} catch (_) {}
263+
264+
return false;
243265
}
244266

245267
/// Check if a Chrome/Chromium process is running (any instance, debug port or not).
@@ -2803,7 +2825,141 @@ end tell
28032825
_eventSubscriptions.remove('Page.frameStoppedLoading');
28042826
}
28052827

2828+
/// For Chrome 146+'s consent-based port (no HTTP endpoints):
2829+
/// Connect via WebSocket to the browser-level CDP endpoint, send
2830+
/// Target.getTargets, find the best matching target, and return its WS URL.
2831+
///
2832+
/// Chrome shows "Allow remote debugging?" dialog on first connection.
2833+
/// We wait up to [timeout] seconds for the user to click Allow.
2834+
Future<String?> _discoverTargetViaConsentPort({
2835+
Duration timeout = const Duration(seconds: 30),
2836+
}) async {
2837+
final wsUrl = 'ws://127.0.0.1:$_port/devtools/browser/${_generateUuid()}';
2838+
WebSocket? ws;
2839+
try {
2840+
ws = await WebSocket.connect(wsUrl).timeout(timeout);
2841+
} catch (_) {
2842+
return null;
2843+
}
2844+
2845+
try {
2846+
// Send Target.getTargets
2847+
const msgId = 1;
2848+
ws.add(jsonEncode({
2849+
'id': msgId,
2850+
'method': 'Target.getTargets',
2851+
'params': {},
2852+
}));
2853+
2854+
// Wait for response
2855+
await for (final msg in ws.timeout(const Duration(seconds: 5))) {
2856+
final data = jsonDecode(msg as String) as Map<String, dynamic>;
2857+
if (data['id'] == msgId) {
2858+
final result = data['result'] as Map<String, dynamic>?;
2859+
final targetInfos = result?['targetInfos'] as List? ?? [];
2860+
final pages = targetInfos
2861+
.whereType<Map>()
2862+
.where((t) => t['type'] == 'page')
2863+
.toList();
2864+
2865+
// Match by URL (same logic as _discoverTarget HTTP path)
2866+
final targetUri = _url.isNotEmpty ? Uri.tryParse(_url) : null;
2867+
final targetHost = targetUri?.host ?? '';
2868+
2869+
// Exact URL match
2870+
if (targetHost.isNotEmpty) {
2871+
for (final t in pages) {
2872+
if (t['url'] == _url) {
2873+
connectedToExistingTab = true;
2874+
return 'ws://127.0.0.1:$_port/devtools/page/${t['targetId']}';
2875+
}
2876+
}
2877+
// Same host match
2878+
for (final t in pages) {
2879+
final tabUri = Uri.tryParse(t['url']?.toString() ?? '');
2880+
if (tabUri != null && tabUri.host == targetHost) {
2881+
connectedToExistingTab = true;
2882+
return 'ws://127.0.0.1:$_port/devtools/page/${t['targetId']}';
2883+
}
2884+
}
2885+
// Same root domain
2886+
final targetParts = targetHost.split('.');
2887+
final targetRoot = targetParts.length >= 2
2888+
? targetParts.sublist(targetParts.length - 2).join('.')
2889+
: targetHost;
2890+
for (final t in pages) {
2891+
final tabUri = Uri.tryParse(t['url']?.toString() ?? '');
2892+
if (tabUri != null) {
2893+
final tabParts = tabUri.host.split('.');
2894+
final tabRoot = tabParts.length >= 2
2895+
? tabParts.sublist(tabParts.length - 2).join('.')
2896+
: tabUri.host;
2897+
if (tabRoot == targetRoot) {
2898+
connectedToExistingTab = true;
2899+
return 'ws://127.0.0.1:$_port/devtools/page/${t['targetId']}';
2900+
}
2901+
}
2902+
}
2903+
}
2904+
2905+
// Blank tab or first non-chrome tab
2906+
for (final t in pages) {
2907+
final tabUrl = t['url']?.toString() ?? '';
2908+
if (tabUrl == 'about:blank') {
2909+
return 'ws://127.0.0.1:$_port/devtools/page/${t['targetId']}';
2910+
}
2911+
}
2912+
if (_url.isEmpty && pages.isNotEmpty) {
2913+
return 'ws://127.0.0.1:$_port/devtools/page/${pages.first['targetId']}';
2914+
}
2915+
2916+
// No match found — create a new tab via Target.createTarget
2917+
ws.add(jsonEncode({
2918+
'id': msgId + 1,
2919+
'method': 'Target.createTarget',
2920+
'params': {'url': _url.isNotEmpty ? _url : 'about:blank'},
2921+
}));
2922+
await for (final msg2 in ws.timeout(const Duration(seconds: 5))) {
2923+
final d2 = jsonDecode(msg2 as String) as Map<String, dynamic>;
2924+
if (d2['id'] == msgId + 1) {
2925+
final targetId = d2['result']?['targetId'] as String?;
2926+
if (targetId != null) {
2927+
return 'ws://127.0.0.1:$_port/devtools/page/$targetId';
2928+
}
2929+
break;
2930+
}
2931+
}
2932+
break;
2933+
}
2934+
}
2935+
} catch (_) {}
2936+
2937+
try {
2938+
await ws.close();
2939+
} catch (_) {}
2940+
return null;
2941+
}
2942+
2943+
/// Generate a random UUID v4.
2944+
static String _generateUuid() {
2945+
final r = Random.secure();
2946+
final bytes = List<int>.generate(16, (_) => r.nextInt(256));
2947+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
2948+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
2949+
String hex(int b) => b.toRadixString(16).padLeft(2, '0');
2950+
return '${hex(bytes[0])}${hex(bytes[1])}${hex(bytes[2])}${hex(bytes[3])}'
2951+
'-${hex(bytes[4])}${hex(bytes[5])}'
2952+
'-${hex(bytes[6])}${hex(bytes[7])}'
2953+
'-${hex(bytes[8])}${hex(bytes[9])}'
2954+
'-${hex(bytes[10])}${hex(bytes[11])}${hex(bytes[12])}${hex(bytes[13])}${hex(bytes[14])}${hex(bytes[15])}';
2955+
}
2956+
28062957
Future<String?> _discoverTarget() async {
2958+
// Chrome 146+ consent port: HTTP endpoints not available, use WebSocket CDP.
2959+
if (_isChrome146ConsentPort) {
2960+
return _discoverTargetViaConsentPort(timeout: const Duration(seconds: 30));
2961+
}
2962+
28072963
// Try multiple times as Chrome may still be starting
28082964
for (var i = 0; i < 10; i++) {
28092965
try {

lib/src/cli/server.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ part 'tool_handlers/bug_report_handlers.dart';
6868
part 'tool_handlers/fixture_handlers.dart';
6969
part 'tool_handlers/explore_handlers.dart';
7070

71-
const String currentVersion = '0.9.17';
71+
const String currentVersion = '0.9.18';
7272

7373
/// Session information for multi-session support
7474
class SessionInfo {

packaging/homebrew/flutter-skill.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
class FlutterSkill < Formula
22
desc "MCP Server for Flutter app automation - AI Agent control for Flutter apps"
33
homepage "https://github.com/ai-dashboad/flutter-skill"
4-
version "0.9.17"
4+
version "0.9.18"
55
license "MIT"
66

77
# Platform-specific native binaries
88
on_macos do
99
on_arm do
10-
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.17/flutter-skill-macos-arm64"
10+
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.18/flutter-skill-macos-arm64"
1111
sha256 "PLACEHOLDER_ARM64_SHA256"
1212
end
1313
on_intel do
14-
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.17/flutter-skill-macos-x64"
14+
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.18/flutter-skill-macos-x64"
1515
sha256 "PLACEHOLDER_X64_SHA256"
1616
end
1717
end
1818

1919
on_linux do
20-
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.17/flutter-skill-linux-x64"
20+
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.18/flutter-skill-linux-x64"
2121
sha256 "PLACEHOLDER_LINUX_SHA256"
2222
end
2323

@@ -48,7 +48,7 @@ def caveats
4848
Note: Your Flutter app needs to include the flutter_skill package.
4949
Add to pubspec.yaml:
5050
dependencies:
51-
flutter_skill: ^0.9.17
51+
flutter_skill: ^0.9.18
5252
EOS
5353
end
5454

packaging/npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "flutter-skill",
33
"mcpName": "io.github.ai-dashboad/flutter-skill",
4-
"version": "0.9.17",
4+
"version": "0.9.18",
55
"description": "MCP Server for app automation - Give your AI Agent eyes and hands inside any app (Flutter, React, Web, Native)",
66
"main": "index.js",
77
"bin": {

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: flutter_skill
22
description: Give your AI Agent eyes and hands inside your Flutter app.
3-
version: 0.9.17
3+
version: 0.9.18
44
homepage: https://github.com/ai-dashboad/flutter-skill
55
repository: https://github.com/ai-dashboad/flutter-skill
66
# publish_to: 'none' # Remove this when ready to publish to pub.dev

sdks/android/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
}
55

66
group = "com.flutterskill"
7-
version = "0.9.17"
7+
version = "0.9.18"
88

99
android {
1010
namespace = "com.flutterskill"

0 commit comments

Comments
 (0)