@adhdev/daemon-core 0.5.3
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/dist/index.d.ts +2662 -0
- package/dist/index.js +11341 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/providers/_builtin/.github/workflows/generate-registry.yml +57 -0
- package/providers/_builtin/COMPATIBILITY.md +217 -0
- package/providers/_builtin/CONTRIBUTING.md +200 -0
- package/providers/_builtin/README.md +119 -0
- package/providers/_builtin/_helpers/index.js +188 -0
- package/providers/_builtin/acp/agentpool/provider.json +54 -0
- package/providers/_builtin/acp/amp/provider.json +52 -0
- package/providers/_builtin/acp/auggie/provider.json +57 -0
- package/providers/_builtin/acp/autodev/provider.json +54 -0
- package/providers/_builtin/acp/autohand/provider.json +52 -0
- package/providers/_builtin/acp/blackbox-ai/provider.json +54 -0
- package/providers/_builtin/acp/claude-agent/provider.json +57 -0
- package/providers/_builtin/acp/cline-acp/provider.json +54 -0
- package/providers/_builtin/acp/codebuddy/provider.json +54 -0
- package/providers/_builtin/acp/codex-cli/provider.json +57 -0
- package/providers/_builtin/acp/corust-agent/provider.json +52 -0
- package/providers/_builtin/acp/crow-cli/provider.json +54 -0
- package/providers/_builtin/acp/cursor-acp/provider.json +54 -0
- package/providers/_builtin/acp/deepagents/provider.json +52 -0
- package/providers/_builtin/acp/dimcode/provider.json +54 -0
- package/providers/_builtin/acp/docker-cagent/provider.json +57 -0
- package/providers/_builtin/acp/factory-droid/provider.json +60 -0
- package/providers/_builtin/acp/fast-agent/provider.json +52 -0
- package/providers/_builtin/acp/gemini-cli/provider.json +114 -0
- package/providers/_builtin/acp/github-copilot/provider.json +54 -0
- package/providers/_builtin/acp/goose/provider.json +57 -0
- package/providers/_builtin/acp/junie/provider.json +52 -0
- package/providers/_builtin/acp/kilo/provider.json +54 -0
- package/providers/_builtin/acp/kimi-cli/provider.json +57 -0
- package/providers/_builtin/acp/minion-code/provider.json +52 -0
- package/providers/_builtin/acp/mistral-vibe/provider.json +57 -0
- package/providers/_builtin/acp/nova/provider.json +54 -0
- package/providers/_builtin/acp/openclaw/provider.json +54 -0
- package/providers/_builtin/acp/opencode/provider.json +52 -0
- package/providers/_builtin/acp/openhands/provider.json +54 -0
- package/providers/_builtin/acp/pi-acp/provider.json +52 -0
- package/providers/_builtin/acp/qoder/provider.json +54 -0
- package/providers/_builtin/acp/qwen-code/provider.json +60 -0
- package/providers/_builtin/acp/stakpak/provider.json +54 -0
- package/providers/_builtin/acp/vtcode/provider.json +54 -0
- package/providers/_builtin/cli/claude-cli/provider.json +100 -0
- package/providers/_builtin/cli/codex-cli/provider.json +89 -0
- package/providers/_builtin/cli/gemini-cli/provider.json +93 -0
- package/providers/_builtin/docs/CDP_SELECTOR_GUIDE.md +370 -0
- package/providers/_builtin/docs/PROVIDER_GUIDE.md +916 -0
- package/providers/_builtin/extension/cline/provider.json +35 -0
- package/providers/_builtin/extension/cline/scripts/focus_editor.js +48 -0
- package/providers/_builtin/extension/cline/scripts/list_chats.js +100 -0
- package/providers/_builtin/extension/cline/scripts/list_models.js +43 -0
- package/providers/_builtin/extension/cline/scripts/list_modes.js +35 -0
- package/providers/_builtin/extension/cline/scripts/new_session.js +85 -0
- package/providers/_builtin/extension/cline/scripts/open_panel.js +25 -0
- package/providers/_builtin/extension/cline/scripts/read_chat.js +257 -0
- package/providers/_builtin/extension/cline/scripts/resolve_action.js +83 -0
- package/providers/_builtin/extension/cline/scripts/send_message.js +95 -0
- package/providers/_builtin/extension/cline/scripts/set_mode.js +36 -0
- package/providers/_builtin/extension/cline/scripts/set_model.js +36 -0
- package/providers/_builtin/extension/cline/scripts/switch_session.js +206 -0
- package/providers/_builtin/extension/cline/scripts.js +73 -0
- package/providers/_builtin/extension/roo-code/provider.json +35 -0
- package/providers/_builtin/extension/roo-code/scripts.js +659 -0
- package/providers/_builtin/ide/antigravity/provider.json +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/focus_editor.js +20 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_chats.js +137 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_models.js +38 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/list_modes.js +48 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/new_session.js +75 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/read_chat.js +262 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/resolve_action.js +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/scripts.js +57 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/send_message.js +56 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/set_mode.js +34 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/set_model.js +47 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/switch_session.js +114 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/focus_editor.js +20 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_chats.js +137 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_models.js +61 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/list_modes.js +72 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/new_session.js +75 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +262 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/resolve_action.js +68 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/scripts.js +67 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/send_message.js +56 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/set_mode.js +67 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/set_model.js +72 -0
- package/providers/_builtin/ide/antigravity/scripts/1.107/switch_session.js +114 -0
- package/providers/_builtin/ide/cursor/provider.json +70 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/dismiss_notification.js +30 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/focus_editor.js +13 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_models.js +78 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_modes.js +40 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_notifications.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/list_sessions.js +42 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/new_session.js +20 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/open_panel.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +75 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/resolve_action.js +19 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/scripts.js +78 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/send_message.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/set_mode.js +38 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/set_model.js +81 -0
- package/providers/_builtin/ide/cursor/scripts/0.49/switch_session.js +28 -0
- package/providers/_builtin/ide/kiro/provider.json +67 -0
- package/providers/_builtin/ide/kiro/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/kiro/scripts/open_panel.js +47 -0
- package/providers/_builtin/ide/kiro/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/kiro/scripts/send_message.js +29 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_models.js +39 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_modes.js +39 -0
- package/providers/_builtin/ide/kiro/scripts/webview_list_sessions.js +21 -0
- package/providers/_builtin/ide/kiro/scripts/webview_new_session.js +34 -0
- package/providers/_builtin/ide/kiro/scripts/webview_read_chat.js +68 -0
- package/providers/_builtin/ide/kiro/scripts/webview_send_message.js +72 -0
- package/providers/_builtin/ide/kiro/scripts/webview_set_mode.js +15 -0
- package/providers/_builtin/ide/kiro/scripts/webview_set_model.js +15 -0
- package/providers/_builtin/ide/kiro/scripts/webview_switch_session.js +26 -0
- package/providers/_builtin/ide/kiro/scripts.js +62 -0
- package/providers/_builtin/ide/pearai/provider.json +67 -0
- package/providers/_builtin/ide/pearai/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/pearai/scripts/list_sessions.js +38 -0
- package/providers/_builtin/ide/pearai/scripts/new_session.js +55 -0
- package/providers/_builtin/ide/pearai/scripts/open_panel.js +46 -0
- package/providers/_builtin/ide/pearai/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/pearai/scripts/send_message.js +29 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_models.js +43 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_modes.js +35 -0
- package/providers/_builtin/ide/pearai/scripts/webview_list_sessions.js +62 -0
- package/providers/_builtin/ide/pearai/scripts/webview_new_session.js +49 -0
- package/providers/_builtin/ide/pearai/scripts/webview_read_chat.js +92 -0
- package/providers/_builtin/ide/pearai/scripts/webview_resolve_action.js +59 -0
- package/providers/_builtin/ide/pearai/scripts/webview_send_message.js +72 -0
- package/providers/_builtin/ide/pearai/scripts/webview_set_mode.js +36 -0
- package/providers/_builtin/ide/pearai/scripts/webview_set_model.js +36 -0
- package/providers/_builtin/ide/pearai/scripts/webview_switch_session.js +34 -0
- package/providers/_builtin/ide/pearai/scripts.js +74 -0
- package/providers/_builtin/ide/trae/provider.json +66 -0
- package/providers/_builtin/ide/trae/scripts/focus_editor.js +20 -0
- package/providers/_builtin/ide/trae/scripts/list_chats.js +24 -0
- package/providers/_builtin/ide/trae/scripts/list_models.js +39 -0
- package/providers/_builtin/ide/trae/scripts/list_modes.js +39 -0
- package/providers/_builtin/ide/trae/scripts/new_session.js +30 -0
- package/providers/_builtin/ide/trae/scripts/open_panel.js +44 -0
- package/providers/_builtin/ide/trae/scripts/read_chat.js +113 -0
- package/providers/_builtin/ide/trae/scripts/resolve_action.js +54 -0
- package/providers/_builtin/ide/trae/scripts/send_message.js +69 -0
- package/providers/_builtin/ide/trae/scripts/set_mode.js +15 -0
- package/providers/_builtin/ide/trae/scripts/set_model.js +15 -0
- package/providers/_builtin/ide/trae/scripts/switch_session.js +23 -0
- package/providers/_builtin/ide/trae/scripts.js +57 -0
- package/providers/_builtin/ide/vscode/provider.json +64 -0
- package/providers/_builtin/ide/vscode-insiders/provider.json +62 -0
- package/providers/_builtin/ide/vscodium/provider.json +63 -0
- package/providers/_builtin/ide/windsurf/provider.json +53 -0
- package/providers/_builtin/ide/windsurf/scripts/focus_editor.js +30 -0
- package/providers/_builtin/ide/windsurf/scripts/list_chats.js +117 -0
- package/providers/_builtin/ide/windsurf/scripts/list_models.js +39 -0
- package/providers/_builtin/ide/windsurf/scripts/list_modes.js +39 -0
- package/providers/_builtin/ide/windsurf/scripts/new_session.js +69 -0
- package/providers/_builtin/ide/windsurf/scripts/open_panel.js +58 -0
- package/providers/_builtin/ide/windsurf/scripts/read_chat.js +297 -0
- package/providers/_builtin/ide/windsurf/scripts/resolve_action.js +68 -0
- package/providers/_builtin/ide/windsurf/scripts/send_message.js +87 -0
- package/providers/_builtin/ide/windsurf/scripts/set_mode.js +15 -0
- package/providers/_builtin/ide/windsurf/scripts/set_model.js +15 -0
- package/providers/_builtin/ide/windsurf/scripts/switch_session.js +58 -0
- package/providers/_builtin/ide/windsurf/scripts.js +57 -0
- package/providers/_builtin/registry.json +266 -0
- package/providers/_builtin/validate.js +156 -0
- package/src/agent-stream/index.ts +6 -0
- package/src/agent-stream/manager.ts +286 -0
- package/src/agent-stream/poller.ts +154 -0
- package/src/agent-stream/provider-adapter.ts +138 -0
- package/src/agent-stream/types.ts +61 -0
- package/src/boot/daemon-lifecycle.ts +252 -0
- package/src/cdp/devtools.ts +335 -0
- package/src/cdp/initializer.ts +191 -0
- package/src/cdp/manager.ts +897 -0
- package/src/cdp/scanner.ts +185 -0
- package/src/cdp/setup.ts +150 -0
- package/src/cli-adapter-types.ts +25 -0
- package/src/cli-adapters/provider-cli-adapter.ts +448 -0
- package/src/commands/cdp-commands.ts +208 -0
- package/src/commands/chat-commands.ts +675 -0
- package/src/commands/cli-manager.ts +353 -0
- package/src/commands/handler.ts +328 -0
- package/src/commands/router.ts +258 -0
- package/src/commands/stream-commands.ts +325 -0
- package/src/config/chat-history.ts +211 -0
- package/src/config/config.ts +219 -0
- package/src/daemon/dev-server.ts +2378 -0
- package/src/daemon/scaffold-template.ts +394 -0
- package/src/daemon-core.ts +50 -0
- package/src/detection/cli-detector.ts +89 -0
- package/src/detection/ide-detector.ts +157 -0
- package/src/index.ts +103 -0
- package/src/installer.ts +263 -0
- package/src/ipc-protocol.ts +133 -0
- package/src/launch.ts +433 -0
- package/src/logging/command-log.ts +180 -0
- package/src/logging/logger.ts +316 -0
- package/src/providers/acp-provider-instance.ts +1140 -0
- package/src/providers/cli-provider-instance.ts +207 -0
- package/src/providers/contracts.ts +524 -0
- package/src/providers/extension-provider-instance.ts +156 -0
- package/src/providers/ide-provider-instance.ts +377 -0
- package/src/providers/index.ts +18 -0
- package/src/providers/provider-instance-manager.ts +182 -0
- package/src/providers/provider-instance.ts +112 -0
- package/src/providers/provider-loader.ts +1031 -0
- package/src/providers/status-monitor.ts +125 -0
- package/src/providers/version-archive.ts +266 -0
- package/src/status/reporter.ts +294 -0
- package/src/types.ts +206 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Manager for ADHDev Daemon
|
|
3
|
+
*
|
|
4
|
+
* Ported cdp.ts from Extension for Daemon use.
|
|
5
|
+
* vscode dependencies removed — works in pure Node.js environment.
|
|
6
|
+
*
|
|
7
|
+
* Connects to IDE CDP port (9222, 9333 etc) to:
|
|
8
|
+
* - Execute JS via Runtime.evaluate
|
|
9
|
+
* - Agent webview iframe search & session connection
|
|
10
|
+
* - DOM query
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import WebSocket from 'ws';
|
|
14
|
+
import * as http from 'http';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import { LOG } from '../logging/logger.js';
|
|
17
|
+
import type { CdpTargetFilter } from '../providers/contracts.js';
|
|
18
|
+
|
|
19
|
+
interface CdpTarget {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
title: string;
|
|
23
|
+
url: string;
|
|
24
|
+
webSocketDebuggerUrl: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentWebviewTarget {
|
|
28
|
+
targetId: string;
|
|
29
|
+
extensionId: string;
|
|
30
|
+
agentType: string;
|
|
31
|
+
url: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export class DaemonCdpManager {
|
|
36
|
+
private ws: WebSocket | null = null;
|
|
37
|
+
private browserWs: WebSocket | null = null; // browser-level WS for Target discovery
|
|
38
|
+
private browserMsgId = 10000;
|
|
39
|
+
private browserPending = new Map<number, {
|
|
40
|
+
resolve: (v: any) => void;
|
|
41
|
+
reject: (e: Error) => void;
|
|
42
|
+
}>();
|
|
43
|
+
private msgId = 1;
|
|
44
|
+
private pending = new Map<number, {
|
|
45
|
+
resolve: (v: any) => void;
|
|
46
|
+
reject: (e: Error) => void;
|
|
47
|
+
}>();
|
|
48
|
+
private port: number;
|
|
49
|
+
private _connected = false;
|
|
50
|
+
private _browserConnected = false;
|
|
51
|
+
private targetUrl = '';
|
|
52
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
53
|
+
private contexts = new Set<number>();
|
|
54
|
+
private connectPromise: Promise<boolean> | null = null;
|
|
55
|
+
private failureCount = 0;
|
|
56
|
+
private readonly MAX_FAILURES = 5;
|
|
57
|
+
private agentSessions = new Map<string, AgentWebviewTarget>();
|
|
58
|
+
private logFn: (msg: string) => void;
|
|
59
|
+
private extensionProviders: { agentType: string; extensionId: string; extensionIdPattern: RegExp }[] = [];
|
|
60
|
+
private _lastDiscoverSig = '';
|
|
61
|
+
private _targetId: string | null = null; // Connect to specific targetId (multi-window support)
|
|
62
|
+
private _pageTitle: string = ''; // Connected page title
|
|
63
|
+
private _targetFilter: CdpTargetFilter; // Provider-configurable target selection
|
|
64
|
+
|
|
65
|
+
constructor(port = 9333, logFn?: (msg: string) => void, targetId?: string, targetFilter?: CdpTargetFilter) {
|
|
66
|
+
this.port = port;
|
|
67
|
+
this._targetId = targetId || null;
|
|
68
|
+
this._targetFilter = targetFilter || {};
|
|
69
|
+
this.logFn = logFn || ((msg) => {
|
|
70
|
+
LOG.info('CDP', msg);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Set target filter (can be updated after construction) */
|
|
75
|
+
setTargetFilter(filter: CdpTargetFilter): void {
|
|
76
|
+
this._targetFilter = filter;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if a page title should be excluded (non-main page).
|
|
81
|
+
* Uses provider-configured titleExcludes, falls back to default pattern.
|
|
82
|
+
*/
|
|
83
|
+
private isNonMainTitle(title: string): boolean {
|
|
84
|
+
if (!title) return true;
|
|
85
|
+
const pattern = this._targetFilter.titleExcludes
|
|
86
|
+
|| 'extension-output|ADHDev CDP|Debug Console|Output\\s*$|Launchpad';
|
|
87
|
+
return new RegExp(pattern, 'i').test(title);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a page URL matches the main window criteria.
|
|
92
|
+
* Uses provider-configured urlIncludes/urlExcludes.
|
|
93
|
+
*/
|
|
94
|
+
private isMainPageUrl(url: string | undefined): boolean {
|
|
95
|
+
if (!url) return true; // no URL filter = accept all
|
|
96
|
+
const { urlIncludes, urlExcludes } = this._targetFilter;
|
|
97
|
+
if (urlIncludes && !url.includes(urlIncludes)) return false;
|
|
98
|
+
if (urlExcludes) {
|
|
99
|
+
for (const exc of urlExcludes) {
|
|
100
|
+
if (url.includes(exc)) return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Connected page title (includes workspace name) */
|
|
107
|
+
get pageTitle(): string { return this._pageTitle; }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Query all workbench pages on port (static)
|
|
111
|
+
* Returns multiple entries if multiple IDE windows are open on same port
|
|
112
|
+
*/
|
|
113
|
+
static listAllTargets(port: number): Promise<CdpTarget[]> {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const req = http.get(`http://127.0.0.1:${port}/json`, (res) => {
|
|
116
|
+
let data = '';
|
|
117
|
+
res.on('data', (chunk: Buffer) => data += chunk.toString());
|
|
118
|
+
res.on('end', () => {
|
|
119
|
+
try {
|
|
120
|
+
const targets: CdpTarget[] = JSON.parse(data);
|
|
121
|
+
const pages = targets.filter(
|
|
122
|
+
t => t.type === 'page' && t.webSocketDebuggerUrl
|
|
123
|
+
);
|
|
124
|
+
// Filter using default target filter (static — no provider filter available)
|
|
125
|
+
const defaultExclude = /extension-output|ADHDev CDP|Debug Console|Output\s*$|Launchpad/i;
|
|
126
|
+
const isNonMain = (title: string) => !title || defaultExclude.test(title);
|
|
127
|
+
const mainPages = pages.filter(t =>
|
|
128
|
+
!isNonMain(t.title || '') &&
|
|
129
|
+
t.url?.includes('workbench.html') &&
|
|
130
|
+
!t.url?.includes('agent')
|
|
131
|
+
);
|
|
132
|
+
const fallbackPages = pages.filter(t => !isNonMain(t.title || ''));
|
|
133
|
+
resolve(mainPages.length > 0 ? mainPages : fallbackPages);
|
|
134
|
+
} catch { resolve([]); }
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
req.on('error', () => resolve([]));
|
|
138
|
+
req.setTimeout(2000, () => { req.destroy(); resolve([]); });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setPort(port: number): void {
|
|
143
|
+
this.port = port;
|
|
144
|
+
this.log(`[CDP] Port changed to ${port}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getPort(): number { return this.port; }
|
|
148
|
+
|
|
149
|
+
private log(msg: string): void {
|
|
150
|
+
this.logFn(msg);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Connection Management ───────────────────────────────
|
|
154
|
+
|
|
155
|
+
async connect(): Promise<boolean> {
|
|
156
|
+
if (this._connected && this.ws?.readyState === WebSocket.OPEN) return true;
|
|
157
|
+
if (this.connectPromise) return this.connectPromise;
|
|
158
|
+
this.connectPromise = this.doConnect();
|
|
159
|
+
try {
|
|
160
|
+
return await this.connectPromise;
|
|
161
|
+
} finally {
|
|
162
|
+
this.connectPromise = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async doConnect(): Promise<boolean> {
|
|
167
|
+
try {
|
|
168
|
+
const target = await this.findTarget();
|
|
169
|
+
if (!target) return false;
|
|
170
|
+
|
|
171
|
+
this.log(`[CDP] Connecting to: ${target.title} (${target.id}) on port ${this.port}`);
|
|
172
|
+
this.targetUrl = target.webSocketDebuggerUrl;
|
|
173
|
+
const ok = await this.connectToTarget(this.targetUrl);
|
|
174
|
+
if (ok) this.log('[CDP] ✅ Connected');
|
|
175
|
+
return ok;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
this.log(`[CDP] Connection error: ${(err as Error).message}`);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private findTargetOnPort(port: number): Promise<CdpTarget | null> {
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
const req = http.get(`http://127.0.0.1:${port}/json`, (res) => {
|
|
185
|
+
let data = '';
|
|
186
|
+
res.on('data', (chunk: Buffer) => data += chunk.toString());
|
|
187
|
+
res.on('end', () => {
|
|
188
|
+
try {
|
|
189
|
+
const targets: CdpTarget[] = JSON.parse(data);
|
|
190
|
+
const pages = targets.filter(
|
|
191
|
+
t => (t.type === 'page' || t.type === 'browser' || t.type === 'Page') && t.webSocketDebuggerUrl
|
|
192
|
+
);
|
|
193
|
+
if (pages.length === 0) {
|
|
194
|
+
resolve(targets.find(t => t.webSocketDebuggerUrl) || null);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Exclude non-main tabs
|
|
199
|
+
const mainPages = pages.filter(t => !this.isNonMainTitle(t.title || ''));
|
|
200
|
+
const list = mainPages.length > 0 ? mainPages : pages;
|
|
201
|
+
|
|
202
|
+
this.log(`[CDP] pages(${list.length}): ${list.map(t => `"${t.title}"`).join(', ')}`);
|
|
203
|
+
|
|
204
|
+
// If targetId is specified, select only matching page
|
|
205
|
+
if (this._targetId) {
|
|
206
|
+
const specific = list.find(t => t.id === this._targetId);
|
|
207
|
+
if (specific) {
|
|
208
|
+
this._pageTitle = specific.title || '';
|
|
209
|
+
resolve(specific);
|
|
210
|
+
} else {
|
|
211
|
+
this.log(`[CDP] Target ${this._targetId} not found in page list`);
|
|
212
|
+
resolve(null);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this._pageTitle = list[0]?.title || '';
|
|
218
|
+
resolve(list[0]);
|
|
219
|
+
} catch { resolve(null); }
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
req.on('error', () => resolve(null));
|
|
223
|
+
req.setTimeout(2000, () => { req.destroy(); resolve(null); });
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private async findTarget(): Promise<CdpTarget | null> {
|
|
228
|
+
return this.findTargetOnPort(this.port);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setExtensionProviders(providers: { agentType: string; extensionId: string; extensionIdPattern: RegExp }[]): void {
|
|
232
|
+
this.extensionProviders = providers;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private connectToTarget(wsUrl: string): Promise<boolean> {
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
this.ws = new WebSocket(wsUrl);
|
|
238
|
+
|
|
239
|
+
this.ws.on('open', async () => {
|
|
240
|
+
this._connected = true;
|
|
241
|
+
try { await this.sendInternal('Runtime.enable'); } catch { }
|
|
242
|
+
// Also connect Browser-level WS (for discovering agent iframes)
|
|
243
|
+
this.connectBrowserWs().catch(() => { });
|
|
244
|
+
resolve(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
this.ws.on('message', (data) => {
|
|
248
|
+
try {
|
|
249
|
+
const msg = JSON.parse(data.toString());
|
|
250
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
251
|
+
const { resolve, reject } = this.pending.get(msg.id)!;
|
|
252
|
+
this.pending.delete(msg.id);
|
|
253
|
+
this.failureCount = 0;
|
|
254
|
+
if (msg.error) reject(new Error(msg.error.message));
|
|
255
|
+
else resolve(msg.result);
|
|
256
|
+
} else if (msg.method === 'Runtime.executionContextCreated') {
|
|
257
|
+
this.contexts.add(msg.params.context.id);
|
|
258
|
+
} else if (msg.method === 'Runtime.executionContextDestroyed') {
|
|
259
|
+
this.contexts.delete(msg.params.executionContextId);
|
|
260
|
+
} else if (msg.method === 'Runtime.executionContextsCleared') {
|
|
261
|
+
this.contexts.clear();
|
|
262
|
+
}
|
|
263
|
+
} catch { }
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.ws.on('close', () => {
|
|
267
|
+
this.log('[CDP] WebSocket closed — scheduling reconnect');
|
|
268
|
+
this._connected = false;
|
|
269
|
+
this._browserConnected = false;
|
|
270
|
+
this.browserWs?.close();
|
|
271
|
+
this.browserWs = null;
|
|
272
|
+
this.connectPromise = null;
|
|
273
|
+
this.scheduleReconnect();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
this.ws.on('error', (err) => {
|
|
277
|
+
this.log(`[CDP] WebSocket error: ${err.message}`);
|
|
278
|
+
this._connected = false;
|
|
279
|
+
resolve(false);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Browser-level CDP connection — needed for Target discovery */
|
|
285
|
+
private async connectBrowserWs(): Promise<void> {
|
|
286
|
+
if (this._browserConnected && this.browserWs?.readyState === WebSocket.OPEN) return;
|
|
287
|
+
try {
|
|
288
|
+
const browserWsUrl = await this.getBrowserWsUrl();
|
|
289
|
+
if (!browserWsUrl) {
|
|
290
|
+
this.log('[CDP] No browser WS URL found');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.log(`[CDP] Connecting browser WS for target discovery...`);
|
|
294
|
+
await new Promise<void>((resolve, reject) => {
|
|
295
|
+
this.browserWs = new WebSocket(browserWsUrl);
|
|
296
|
+
this.browserWs.on('open', async () => {
|
|
297
|
+
this._browserConnected = true;
|
|
298
|
+
this.log('[CDP] ✅ Browser WS connected — enabling target discovery');
|
|
299
|
+
try {
|
|
300
|
+
await this.sendBrowser('Target.setDiscoverTargets', { discover: true });
|
|
301
|
+
} catch (e) {
|
|
302
|
+
this.log(`[CDP] setDiscoverTargets failed: ${(e as Error).message}`);
|
|
303
|
+
}
|
|
304
|
+
resolve();
|
|
305
|
+
});
|
|
306
|
+
this.browserWs.on('message', (data) => {
|
|
307
|
+
try {
|
|
308
|
+
const msg = JSON.parse(data.toString());
|
|
309
|
+
if (msg.id && this.browserPending.has(msg.id)) {
|
|
310
|
+
const { resolve, reject } = this.browserPending.get(msg.id)!;
|
|
311
|
+
this.browserPending.delete(msg.id);
|
|
312
|
+
if (msg.error) reject(new Error(msg.error.message));
|
|
313
|
+
else resolve(msg.result);
|
|
314
|
+
}
|
|
315
|
+
} catch { }
|
|
316
|
+
});
|
|
317
|
+
this.browserWs.on('close', () => {
|
|
318
|
+
this._browserConnected = false;
|
|
319
|
+
this.browserWs = null;
|
|
320
|
+
});
|
|
321
|
+
this.browserWs.on('error', (err) => {
|
|
322
|
+
this.log(`[CDP] Browser WS error: ${err.message}`);
|
|
323
|
+
this._browserConnected = false;
|
|
324
|
+
reject(err);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
} catch (e) {
|
|
328
|
+
this.log(`[CDP] Browser WS connect failed: ${(e as Error).message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private getBrowserWsUrl(): Promise<string | null> {
|
|
333
|
+
return new Promise((resolve) => {
|
|
334
|
+
const req = http.get(`http://127.0.0.1:${this.port}/json/version`, (res) => {
|
|
335
|
+
let data = '';
|
|
336
|
+
res.on('data', (chunk: Buffer) => data += chunk.toString());
|
|
337
|
+
res.on('end', () => {
|
|
338
|
+
try {
|
|
339
|
+
const info = JSON.parse(data);
|
|
340
|
+
resolve(info.webSocketDebuggerUrl || null);
|
|
341
|
+
} catch { resolve(null); }
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
req.on('error', () => resolve(null));
|
|
345
|
+
req.setTimeout(3000, () => { req.destroy(); resolve(null); });
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private sendBrowser(method: string, params: Record<string, unknown> = {}, timeoutMs = 15000): Promise<any> {
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
if (!this.browserWs || !this._browserConnected) return reject(new Error('Browser WS not connected'));
|
|
352
|
+
const id = this.browserMsgId++;
|
|
353
|
+
this.browserPending.set(id, { resolve, reject });
|
|
354
|
+
this.browserWs.send(JSON.stringify({ id, method, params }));
|
|
355
|
+
setTimeout(() => {
|
|
356
|
+
if (this.browserPending.has(id)) {
|
|
357
|
+
this.browserPending.delete(id);
|
|
358
|
+
reject(new Error(`Browser CDP timeout: ${method}`));
|
|
359
|
+
}
|
|
360
|
+
}, timeoutMs);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private scheduleReconnect(): void {
|
|
365
|
+
if (this.reconnectTimer) return;
|
|
366
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
367
|
+
this.reconnectTimer = null;
|
|
368
|
+
if (!this._connected) {
|
|
369
|
+
const ok = await this.connect();
|
|
370
|
+
// Schedule reconnect on connection failure (prevent infinite loop: only when port is alive)
|
|
371
|
+
if (!ok && !this._connected) {
|
|
372
|
+
this.scheduleReconnect();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}, 5000);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
disconnect(): void {
|
|
379
|
+
if (this.reconnectTimer) {
|
|
380
|
+
clearTimeout(this.reconnectTimer);
|
|
381
|
+
this.reconnectTimer = null;
|
|
382
|
+
}
|
|
383
|
+
this.ws?.close();
|
|
384
|
+
this.ws = null;
|
|
385
|
+
this._connected = false;
|
|
386
|
+
this.browserWs?.close();
|
|
387
|
+
this.browserWs = null;
|
|
388
|
+
this._browserConnected = false;
|
|
389
|
+
this.failureCount = 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
get isConnected(): boolean {
|
|
393
|
+
return this._connected || this.ws?.readyState === WebSocket.OPEN;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─── CDP Protocol ────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
private sendInternal(method: string, params: Record<string, unknown> = {}, timeoutMs = 15000): Promise<any> {
|
|
399
|
+
return new Promise((resolve, reject) => {
|
|
400
|
+
if (!this.ws || !this._connected) return reject(new Error('CDP not connected'));
|
|
401
|
+
if (this.ws.readyState !== WebSocket.OPEN) return reject(new Error('WebSocket not open'));
|
|
402
|
+
|
|
403
|
+
const id = this.msgId++;
|
|
404
|
+
this.pending.set(id, { resolve, reject });
|
|
405
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
406
|
+
|
|
407
|
+
setTimeout(() => {
|
|
408
|
+
if (this.pending.has(id)) {
|
|
409
|
+
this.pending.delete(id);
|
|
410
|
+
this.failureCount++;
|
|
411
|
+
if (this.failureCount >= this.MAX_FAILURES) {
|
|
412
|
+
this.log(`[CDP] Force-disconnecting: ${this.failureCount} timeouts (last: ${method})`);
|
|
413
|
+
this.disconnect();
|
|
414
|
+
}
|
|
415
|
+
reject(new Error(`CDP timeout: ${method}`));
|
|
416
|
+
}
|
|
417
|
+
}, timeoutMs);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
send(method: string, params: Record<string, unknown> = {}, timeoutMs = 15000): Promise<any> {
|
|
422
|
+
return this.sendInternal(method, params, timeoutMs);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async sendCdpCommand(method: string, params: Record<string, unknown> = {}): Promise<any> {
|
|
426
|
+
return this.sendInternal(method, params);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async evaluate(expression: string, timeoutMs = 30000): Promise<unknown> {
|
|
430
|
+
try {
|
|
431
|
+
const { result } = await this.sendInternal('Runtime.evaluate', {
|
|
432
|
+
expression,
|
|
433
|
+
returnByValue: true,
|
|
434
|
+
awaitPromise: true,
|
|
435
|
+
}, timeoutMs);
|
|
436
|
+
if (result.subtype === 'error') throw new Error(result.description);
|
|
437
|
+
this.failureCount = 0;
|
|
438
|
+
return result.value;
|
|
439
|
+
} catch (e) {
|
|
440
|
+
const isTimeout = (e as Error).message?.includes('timeout');
|
|
441
|
+
if (isTimeout) throw e;
|
|
442
|
+
|
|
443
|
+
for (const ctxId of this.contexts) {
|
|
444
|
+
try {
|
|
445
|
+
const { result } = await this.sendInternal('Runtime.evaluate', {
|
|
446
|
+
expression,
|
|
447
|
+
returnByValue: true,
|
|
448
|
+
awaitPromise: true,
|
|
449
|
+
contextId: ctxId,
|
|
450
|
+
});
|
|
451
|
+
if (result.subtype === 'error') continue;
|
|
452
|
+
return result.value;
|
|
453
|
+
} catch { continue; }
|
|
454
|
+
}
|
|
455
|
+
throw e;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async querySelector(selector: string): Promise<string | null> {
|
|
460
|
+
return await this.evaluate(`
|
|
461
|
+
(() => {
|
|
462
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
463
|
+
return el ? el.outerHTML.substring(0, 2000) : null;
|
|
464
|
+
})()
|
|
465
|
+
`) as string | null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Input text via CDP protocol then send Enter
|
|
470
|
+
* Used for editors where execCommand does not work (e.g. Lexical).
|
|
471
|
+
*
|
|
472
|
+
* 1. Find editor by selector, focus + click
|
|
473
|
+
* 2. Insert text via Input.insertText
|
|
474
|
+
* 3. Send Enter via Input.dispatchKeyEvent
|
|
475
|
+
*/
|
|
476
|
+
async typeAndSend(selector: string, text: string): Promise<boolean> {
|
|
477
|
+
if (!this.isConnected) return false;
|
|
478
|
+
|
|
479
|
+
// Step 1: Focus + get position
|
|
480
|
+
const focusResult = await this.evaluate(`(() => {
|
|
481
|
+
const e = document.querySelector(${JSON.stringify(selector)});
|
|
482
|
+
if (!e) return null;
|
|
483
|
+
e.focus();
|
|
484
|
+
const r = e.getBoundingClientRect();
|
|
485
|
+
return JSON.stringify({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
486
|
+
})()`) as string | null;
|
|
487
|
+
|
|
488
|
+
if (!focusResult) {
|
|
489
|
+
this.log('[CDP] typeAndSend: selector not found');
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const pos = JSON.parse(focusResult);
|
|
494
|
+
|
|
495
|
+
// Step 2: Click to ensure focus
|
|
496
|
+
await this.sendInternal('Input.dispatchMouseEvent', {
|
|
497
|
+
type: 'mousePressed', x: Math.round(pos.x), y: Math.round(pos.y),
|
|
498
|
+
button: 'left', clickCount: 1
|
|
499
|
+
});
|
|
500
|
+
await this.sendInternal('Input.dispatchMouseEvent', {
|
|
501
|
+
type: 'mouseReleased', x: Math.round(pos.x), y: Math.round(pos.y),
|
|
502
|
+
button: 'left', clickCount: 1
|
|
503
|
+
});
|
|
504
|
+
await new Promise(r => setTimeout(r, 150));
|
|
505
|
+
|
|
506
|
+
// Step 3: Insert text
|
|
507
|
+
await this.sendInternal('Input.insertText', { text });
|
|
508
|
+
await new Promise(r => setTimeout(r, 200));
|
|
509
|
+
|
|
510
|
+
// Step 4: Press Enter
|
|
511
|
+
await this.sendInternal('Input.dispatchKeyEvent', {
|
|
512
|
+
type: 'rawKeyDown', key: 'Enter', code: 'Enter',
|
|
513
|
+
windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13,
|
|
514
|
+
});
|
|
515
|
+
await this.sendInternal('Input.dispatchKeyEvent', {
|
|
516
|
+
type: 'keyUp', key: 'Enter', code: 'Enter',
|
|
517
|
+
windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
this.log(`[CDP] typeAndSend: sent "${text.substring(0, 50)}..."`);
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Coordinate-based typeAndSend — for input fields inside webview iframe
|
|
526
|
+
* Receives coordinates directly instead of selector for click+input+Enter
|
|
527
|
+
*/
|
|
528
|
+
async typeAndSendAt(x: number, y: number, text: string): Promise<boolean> {
|
|
529
|
+
if (!this.isConnected) return false;
|
|
530
|
+
|
|
531
|
+
// Step 1: Click to focus
|
|
532
|
+
await this.sendInternal('Input.dispatchMouseEvent', {
|
|
533
|
+
type: 'mousePressed', x: Math.round(x), y: Math.round(y),
|
|
534
|
+
button: 'left', clickCount: 1
|
|
535
|
+
});
|
|
536
|
+
await this.sendInternal('Input.dispatchMouseEvent', {
|
|
537
|
+
type: 'mouseReleased', x: Math.round(x), y: Math.round(y),
|
|
538
|
+
button: 'left', clickCount: 1
|
|
539
|
+
});
|
|
540
|
+
await new Promise(r => setTimeout(r, 300));
|
|
541
|
+
|
|
542
|
+
// Step 2: Select all + delete (remove existing content)
|
|
543
|
+
await this.sendInternal('Input.dispatchKeyEvent', {
|
|
544
|
+
type: 'rawKeyDown', key: 'a', code: 'KeyA',
|
|
545
|
+
windowsVirtualKeyCode: 65, modifiers: 8, // Meta
|
|
546
|
+
});
|
|
547
|
+
await this.sendInternal('Input.dispatchKeyEvent', {
|
|
548
|
+
type: 'keyUp', key: 'a', code: 'KeyA',
|
|
549
|
+
windowsVirtualKeyCode: 65, modifiers: 8,
|
|
550
|
+
});
|
|
551
|
+
await this.sendInternal('Input.dispatchKeyEvent', {
|
|
552
|
+
type: 'rawKeyDown', key: 'Backspace', code: 'Backspace',
|
|
553
|
+
windowsVirtualKeyCode: 8,
|
|
554
|
+
});
|
|
555
|
+
await this.sendInternal('Input.dispatchKeyEvent', {
|
|
556
|
+
type: 'keyUp', key: 'Backspace', code: 'Backspace',
|
|
557
|
+
windowsVirtualKeyCode: 8,
|
|
558
|
+
});
|
|
559
|
+
await new Promise(r => setTimeout(r, 150));
|
|
560
|
+
|
|
561
|
+
// Step 3: Insert text
|
|
562
|
+
await this.sendInternal('Input.insertText', { text });
|
|
563
|
+
await new Promise(r => setTimeout(r, 200));
|
|
564
|
+
|
|
565
|
+
// Step 4: Press Enter
|
|
566
|
+
await this.sendInternal('Input.dispatchKeyEvent', {
|
|
567
|
+
type: 'rawKeyDown', key: 'Enter', code: 'Enter',
|
|
568
|
+
windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13,
|
|
569
|
+
});
|
|
570
|
+
await this.sendInternal('Input.dispatchKeyEvent', {
|
|
571
|
+
type: 'keyUp', key: 'Enter', code: 'Enter',
|
|
572
|
+
windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
this.log(`[CDP] typeAndSendAt(${Math.round(x)},${Math.round(y)}): sent "${text.substring(0, 50)}..."`);
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Evaluate JS from inside Webview iframe
|
|
581
|
+
* Kiro, PearAI etc Used for IDEs where chat UI is inside webview iframe.
|
|
582
|
+
*
|
|
583
|
+
* 1. Query Target.getTargets via browser WS → find vscode-webview iframes
|
|
584
|
+
* 2. Target.attachToTarget → session acquire
|
|
585
|
+
* 3. Page.getFrameTree → nested iframe find
|
|
586
|
+
* 4. Page.createIsolatedWorld → contextId acquire
|
|
587
|
+
* 5. Runtime.evaluate → result return
|
|
588
|
+
*
|
|
589
|
+
* @param expression JS expression to execute
|
|
590
|
+
* @param matchFn webview iframe URL match function (optional, all webview attempt)
|
|
591
|
+
* @returns evaluate result or null
|
|
592
|
+
*/
|
|
593
|
+
async evaluateInWebviewFrame(expression: string, matchFn?: (bodyPreview: string) => boolean): Promise<string | null> {
|
|
594
|
+
if (!this._browserConnected) {
|
|
595
|
+
await this.connectBrowserWs().catch(() => { });
|
|
596
|
+
}
|
|
597
|
+
if (!this.browserWs || !this._browserConnected) {
|
|
598
|
+
this.log('[CDP] evaluateInWebviewFrame: no browser WS');
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const browserWs = this.browserWs;
|
|
603
|
+
let msgId = this.browserMsgId;
|
|
604
|
+
|
|
605
|
+
const sendWs = (method: string, params: Record<string, unknown> = {}, sessionId?: string): Promise<any> => {
|
|
606
|
+
return new Promise((resolve, reject) => {
|
|
607
|
+
const mid = msgId++;
|
|
608
|
+
this.browserMsgId = msgId;
|
|
609
|
+
const handler = (raw: WebSocket.Data) => {
|
|
610
|
+
try {
|
|
611
|
+
const msg = JSON.parse(raw.toString());
|
|
612
|
+
if (msg.id === mid) {
|
|
613
|
+
browserWs.removeListener('message', handler);
|
|
614
|
+
if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
615
|
+
else resolve(msg.result);
|
|
616
|
+
}
|
|
617
|
+
} catch { /* skip non-JSON */ }
|
|
618
|
+
};
|
|
619
|
+
browserWs.on('message', handler);
|
|
620
|
+
const payload: any = { id: mid, method, params };
|
|
621
|
+
if (sessionId) payload.sessionId = sessionId;
|
|
622
|
+
browserWs.send(JSON.stringify(payload));
|
|
623
|
+
setTimeout(() => {
|
|
624
|
+
browserWs.removeListener('message', handler);
|
|
625
|
+
reject(new Error(`timeout: ${method}`));
|
|
626
|
+
}, 10000);
|
|
627
|
+
});
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
// 1. Find webview iframe targets
|
|
632
|
+
const { targetInfos } = await sendWs('Target.getTargets');
|
|
633
|
+
const webviewIframes = (targetInfos || []).filter(
|
|
634
|
+
(t: any) => t.type === 'iframe' && (t.url || '').includes('vscode-webview')
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
if (webviewIframes.length === 0) {
|
|
638
|
+
this.log('[CDP] evaluateInWebviewFrame: no webview iframes found');
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 2. Try each webview iframe
|
|
643
|
+
for (const iframe of webviewIframes) {
|
|
644
|
+
let sessionId: string | undefined;
|
|
645
|
+
try {
|
|
646
|
+
const attached = await sendWs('Target.attachToTarget', {
|
|
647
|
+
targetId: iframe.targetId, flatten: true,
|
|
648
|
+
});
|
|
649
|
+
sessionId = attached.sessionId;
|
|
650
|
+
|
|
651
|
+
// 3. Get frame tree (nested iframe)
|
|
652
|
+
const { frameTree } = await sendWs('Page.getFrameTree', {}, sessionId);
|
|
653
|
+
const childFrame = frameTree?.childFrames?.[0]?.frame;
|
|
654
|
+
if (!childFrame) {
|
|
655
|
+
await sendWs('Target.detachFromTarget', { sessionId }).catch(() => { });
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// 4. Create isolated world in child frame
|
|
660
|
+
const { executionContextId } = await sendWs('Page.createIsolatedWorld', {
|
|
661
|
+
frameId: childFrame.id,
|
|
662
|
+
worldName: 'adhdev-eval',
|
|
663
|
+
grantUniveralAccess: true,
|
|
664
|
+
}, sessionId);
|
|
665
|
+
|
|
666
|
+
// 5. If matchFn provided, check body content first
|
|
667
|
+
if (matchFn) {
|
|
668
|
+
const checkResult = await sendWs('Runtime.evaluate', {
|
|
669
|
+
expression: `document.documentElement?.outerHTML?.substring(0, 500000) || ''`,
|
|
670
|
+
returnByValue: true,
|
|
671
|
+
contextId: executionContextId,
|
|
672
|
+
}, sessionId);
|
|
673
|
+
const bodyText = checkResult?.result?.value || '';
|
|
674
|
+
if (!matchFn(bodyText)) {
|
|
675
|
+
await sendWs('Target.detachFromTarget', { sessionId }).catch(() => { });
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// 6. Evaluate the expression
|
|
681
|
+
const result = await sendWs('Runtime.evaluate', {
|
|
682
|
+
expression,
|
|
683
|
+
returnByValue: true,
|
|
684
|
+
awaitPromise: true,
|
|
685
|
+
contextId: executionContextId,
|
|
686
|
+
}, sessionId);
|
|
687
|
+
|
|
688
|
+
await sendWs('Target.detachFromTarget', { sessionId }).catch(() => { });
|
|
689
|
+
|
|
690
|
+
const value = result?.result?.value;
|
|
691
|
+
if (value != null) {
|
|
692
|
+
this.log(`[CDP] evaluateInWebviewFrame: success in ${iframe.targetId.substring(0, 12)}`);
|
|
693
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
694
|
+
}
|
|
695
|
+
} catch (e: any) {
|
|
696
|
+
if (sessionId) {
|
|
697
|
+
await sendWs('Target.detachFromTarget', { sessionId }).catch(() => { });
|
|
698
|
+
}
|
|
699
|
+
this.log(`[CDP] evaluateInWebviewFrame: error in ${iframe.targetId.substring(0, 12)}: ${e.message}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
this.log('[CDP] evaluateInWebviewFrame: no matching webview found');
|
|
704
|
+
return null;
|
|
705
|
+
} catch (e: any) {
|
|
706
|
+
this.log(`[CDP] evaluateInWebviewFrame error: ${e.message}`);
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ─── Agent Webview Multi-Session ─────────────────────────
|
|
712
|
+
|
|
713
|
+
async discoverAgentWebviews(): Promise<AgentWebviewTarget[]> {
|
|
714
|
+
if (!this.isConnected) return [];
|
|
715
|
+
|
|
716
|
+
// Retry connection if no Browser WS
|
|
717
|
+
if (!this._browserConnected) {
|
|
718
|
+
await this.connectBrowserWs().catch(() => { });
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
// Query targets from Browser-level WS (includes iframes)
|
|
723
|
+
let allTargets: any[] = [];
|
|
724
|
+
if (this._browserConnected) {
|
|
725
|
+
const result = await this.sendBrowser('Target.getTargets');
|
|
726
|
+
allTargets = result?.targetInfos || [];
|
|
727
|
+
} else {
|
|
728
|
+
// Page-level query (when no browser WS, iframes may not be visible)
|
|
729
|
+
const result = await this.sendInternal('Target.getTargets');
|
|
730
|
+
allTargets = result?.targetInfos || [];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const iframes = allTargets.filter((t: any) => t.type === 'iframe');
|
|
734
|
+
const typeMap = new Map<string, number>();
|
|
735
|
+
for (const t of allTargets) {
|
|
736
|
+
typeMap.set(t.type, (typeMap.get(t.type) || 0) + 1);
|
|
737
|
+
}
|
|
738
|
+
const typeSummary = [...typeMap.entries()].map(([k, v]) => `${k}:${v}`).join(',');
|
|
739
|
+
// Log only on change (called every 5s repeatedly, prevent noise)
|
|
740
|
+
const sig = `${allTargets.length}:${iframes.length}:${typeSummary}`;
|
|
741
|
+
if (sig !== this._lastDiscoverSig) {
|
|
742
|
+
this._lastDiscoverSig = sig;
|
|
743
|
+
this.log(`[CDP] discoverAgentWebviews: ${allTargets.length} total [${typeSummary}], ${iframes.length} iframes (browser=${this._browserConnected})`);
|
|
744
|
+
// Detailed webview target logging also only on change
|
|
745
|
+
for (const t of allTargets) {
|
|
746
|
+
if (t.type !== 'page' && t.type !== 'worker' && t.type !== 'service_worker') {
|
|
747
|
+
this.log(`[CDP] target: type=${t.type} url=${(t.url || '').substring(0, 120)}`);
|
|
748
|
+
}
|
|
749
|
+
if ((t.url || '').includes('vscode-webview')) {
|
|
750
|
+
this.log(`[CDP] webview: type=${t.type} url=${(t.url || '').substring(0, 150)}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const agents: AgentWebviewTarget[] = [];
|
|
756
|
+
for (const target of allTargets) {
|
|
757
|
+
if (target.type !== 'iframe') continue;
|
|
758
|
+
const url = target.url || '';
|
|
759
|
+
const hasWebview = url.includes('vscode-webview');
|
|
760
|
+
if (!hasWebview) continue;
|
|
761
|
+
|
|
762
|
+
for (const known of this.extensionProviders) {
|
|
763
|
+
if (known.extensionIdPattern.test(url)) {
|
|
764
|
+
agents.push({
|
|
765
|
+
targetId: target.targetId,
|
|
766
|
+
extensionId: known.extensionId,
|
|
767
|
+
agentType: known.agentType,
|
|
768
|
+
url: url,
|
|
769
|
+
});
|
|
770
|
+
this.log(`[CDP] Found agent: ${known.agentType} (${target.targetId})`);
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return agents;
|
|
776
|
+
} catch (e) {
|
|
777
|
+
this.log(`[CDP] discoverAgentWebviews error: ${(e as Error).message}`);
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async attachToAgent(target: AgentWebviewTarget): Promise<string | null> {
|
|
783
|
+
if (!this.isConnected) return null;
|
|
784
|
+
for (const [sid, t] of this.agentSessions) {
|
|
785
|
+
if (t.agentType === target.agentType) return sid;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
// Attach via Browser WS (iframes can only be attached from browser-level)
|
|
789
|
+
const sendFn = this._browserConnected ? this.sendBrowser.bind(this) : this.sendInternal.bind(this);
|
|
790
|
+
const result = await sendFn('Target.attachToTarget', {
|
|
791
|
+
targetId: target.targetId,
|
|
792
|
+
flatten: true,
|
|
793
|
+
});
|
|
794
|
+
const sessionId = result?.sessionId;
|
|
795
|
+
if (sessionId) {
|
|
796
|
+
this.agentSessions.set(sessionId, target);
|
|
797
|
+
this.log(`[CDP] Attached to ${target.agentType}, session=${sessionId.substring(0, 12)}...`);
|
|
798
|
+
}
|
|
799
|
+
return sessionId || null;
|
|
800
|
+
} catch (e) {
|
|
801
|
+
this.log(`[CDP] attach error (${target.agentType}): ${(e as Error).message}`);
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async evaluateInSession(sessionId: string, expression: string, timeoutMs = 15000): Promise<unknown> {
|
|
807
|
+
// Flatten mode: if session was opened from same WS, must evaluate via same WS
|
|
808
|
+
const ws = this._browserConnected ? this.browserWs : this.ws;
|
|
809
|
+
const pendingMap = this._browserConnected ? this.browserPending : this.pending;
|
|
810
|
+
const getNextId = () => this._browserConnected ? this.browserMsgId++ : this.msgId++;
|
|
811
|
+
|
|
812
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
813
|
+
throw new Error('CDP not connected');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return new Promise((resolve, reject) => {
|
|
817
|
+
const id = getNextId();
|
|
818
|
+
pendingMap.set(id, {
|
|
819
|
+
resolve: (result: any) => {
|
|
820
|
+
if (result?.result?.subtype === 'error') {
|
|
821
|
+
reject(new Error(result.result.description));
|
|
822
|
+
} else {
|
|
823
|
+
resolve(result?.result?.value);
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
reject,
|
|
827
|
+
});
|
|
828
|
+
ws.send(JSON.stringify({
|
|
829
|
+
id, sessionId,
|
|
830
|
+
method: 'Runtime.evaluate',
|
|
831
|
+
params: { expression, returnByValue: true, awaitPromise: true },
|
|
832
|
+
}));
|
|
833
|
+
setTimeout(() => {
|
|
834
|
+
if (pendingMap.has(id)) {
|
|
835
|
+
pendingMap.delete(id);
|
|
836
|
+
reject(new Error(`CDP agent timeout: ${sessionId.substring(0, 12)}...`));
|
|
837
|
+
}
|
|
838
|
+
}, timeoutMs);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async detachAgent(sessionId: string): Promise<void> {
|
|
843
|
+
try {
|
|
844
|
+
const sendFn = this._browserConnected ? this.sendBrowser.bind(this) : this.sendInternal.bind(this);
|
|
845
|
+
await sendFn('Target.detachFromTarget', { sessionId });
|
|
846
|
+
} catch { }
|
|
847
|
+
this.agentSessions.delete(sessionId);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async detachAllAgents(): Promise<void> {
|
|
851
|
+
for (const sid of Array.from(this.agentSessions.keys())) {
|
|
852
|
+
await this.detachAgent(sid);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
getAgentSessions(): Map<string, AgentWebviewTarget> {
|
|
857
|
+
return this.agentSessions;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ─── Screenshot ──────────────────────────────────────────
|
|
861
|
+
|
|
862
|
+
async captureScreenshot(opts?: { quality?: number }): Promise<Buffer | null> {
|
|
863
|
+
if (!this.isConnected) return null;
|
|
864
|
+
const quality = opts?.quality ?? 20;
|
|
865
|
+
try {
|
|
866
|
+
// Get viewport size for per-clipping pro (avoids HiDPI bloat)
|
|
867
|
+
let clip: any;
|
|
868
|
+
try {
|
|
869
|
+
const metrics = await this.sendInternal('Page.getLayoutMetrics', {}, 3000);
|
|
870
|
+
const vp = metrics?.cssVisualViewport || metrics?.visualViewport;
|
|
871
|
+
if (vp) {
|
|
872
|
+
clip = {
|
|
873
|
+
x: 0, y: 0,
|
|
874
|
+
width: Math.round(vp.clientWidth || vp.width || 1920),
|
|
875
|
+
height: Math.round(vp.clientHeight || vp.height || 1080),
|
|
876
|
+
scale: 1,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
} catch { /* fallback: no clip */ }
|
|
880
|
+
|
|
881
|
+
const result = await this.sendInternal('Page.captureScreenshot', {
|
|
882
|
+
format: 'webp',
|
|
883
|
+
quality,
|
|
884
|
+
...(clip ? { clip } : {}),
|
|
885
|
+
optimizeForSpeed: true,
|
|
886
|
+
captureBeyondViewport: false,
|
|
887
|
+
}, 10000);
|
|
888
|
+
if (result?.data) {
|
|
889
|
+
return Buffer.from(result.data, 'base64');
|
|
890
|
+
}
|
|
891
|
+
return null;
|
|
892
|
+
} catch (e) {
|
|
893
|
+
this.log(`[CDP] Screenshot error: ${(e as Error).message}`);
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|