@sensaiorg/adapter-chrome 0.1.0
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/chrome-adapter.d.ts.map +1 -0
- package/dist/chrome-adapter.js +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/tools/chrome-tools.d.ts.map +1 -0
- package/dist/tools/chrome-tools.js +583 -0
- package/dist/tools/chrome-tools.test.d.ts.map +1 -0
- package/dist/tools/chrome-tools.test.js +442 -0
- package/dist/transport/cdp-bridge.d.ts.map +1 -0
- package/dist/transport/cdp-bridge.js +163 -0
- package/package.json +23 -0
- package/src/chrome-adapter.ts +117 -0
- package/src/index.ts +8 -0
- package/src/tools/chrome-tools.test.ts +547 -0
- package/src/tools/chrome-tools.ts +743 -0
- package/src/transport/cdp-bridge.ts +187 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CdpBridge — WebSocket bridge to the SensAI Chrome Extension.
|
|
3
|
+
*
|
|
4
|
+
* The extension runs a WebSocket server that proxies CDP commands
|
|
5
|
+
* through chrome.debugger API. This bridge connects to that server
|
|
6
|
+
* and provides a simple request/response API.
|
|
7
|
+
*
|
|
8
|
+
* Protocol: JSON-RPC 2.0 over WebSocket (same as Android agent protocol).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import WebSocket from "ws";
|
|
12
|
+
|
|
13
|
+
interface PendingRequest {
|
|
14
|
+
resolve: (result: unknown) => void;
|
|
15
|
+
reject: (error: Error) => void;
|
|
16
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type EventHandler = (params: Record<string, unknown>) => void;
|
|
20
|
+
|
|
21
|
+
export class CdpBridge {
|
|
22
|
+
private ws: WebSocket | null = null;
|
|
23
|
+
private requestId = 0;
|
|
24
|
+
private readonly pendingRequests = new Map<number, PendingRequest>();
|
|
25
|
+
private readonly eventListeners = new Map<string, EventHandler[]>();
|
|
26
|
+
private readonly wsPort: number;
|
|
27
|
+
private connected = false;
|
|
28
|
+
|
|
29
|
+
constructor(wsPort: number) {
|
|
30
|
+
this.wsPort = wsPort;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register a handler for a CDP event (e.g., "Runtime.consoleAPICalled").
|
|
35
|
+
*/
|
|
36
|
+
onEvent(method: string, handler: EventHandler): void {
|
|
37
|
+
const handlers = this.eventListeners.get(method) ?? [];
|
|
38
|
+
handlers.push(handler);
|
|
39
|
+
this.eventListeners.set(method, handlers);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Connect to the Chrome Extension's WebSocket server.
|
|
44
|
+
*/
|
|
45
|
+
async connect(): Promise<boolean> {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
try {
|
|
48
|
+
this.ws = new WebSocket(`ws://localhost:${this.wsPort}`);
|
|
49
|
+
|
|
50
|
+
const connectionTimeout = setTimeout(() => {
|
|
51
|
+
this.ws?.close();
|
|
52
|
+
this.connected = false;
|
|
53
|
+
resolve(false);
|
|
54
|
+
}, 5000);
|
|
55
|
+
|
|
56
|
+
this.ws.on("open", () => {
|
|
57
|
+
clearTimeout(connectionTimeout);
|
|
58
|
+
this.connected = true;
|
|
59
|
+
process.stderr.write(`[sensai:chrome] Connected to extension on port ${this.wsPort}\n`);
|
|
60
|
+
resolve(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.ws.on("message", (data: WebSocket.Data) => {
|
|
64
|
+
this.handleMessage(data.toString());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.ws.on("close", () => {
|
|
68
|
+
this.connected = false;
|
|
69
|
+
this.rejectAllPending("WebSocket connection closed");
|
|
70
|
+
process.stderr.write("[sensai:chrome] Extension disconnected\n");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.ws.on("error", () => {
|
|
74
|
+
clearTimeout(connectionTimeout);
|
|
75
|
+
this.connected = false;
|
|
76
|
+
resolve(false);
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
this.connected = false;
|
|
80
|
+
resolve(false);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Send a CDP command through the extension and wait for response.
|
|
87
|
+
*/
|
|
88
|
+
async send(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
89
|
+
if (!this.ws || !this.connected) {
|
|
90
|
+
throw new Error("Not connected to Chrome Extension");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const id = ++this.requestId;
|
|
94
|
+
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const timeout = setTimeout(() => {
|
|
97
|
+
this.pendingRequests.delete(id);
|
|
98
|
+
reject(new Error(`CDP request timed out: ${method}`));
|
|
99
|
+
}, 30_000);
|
|
100
|
+
|
|
101
|
+
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
102
|
+
|
|
103
|
+
this.ws!.send(JSON.stringify({
|
|
104
|
+
jsonrpc: "2.0",
|
|
105
|
+
id,
|
|
106
|
+
method,
|
|
107
|
+
params: params ?? {},
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the current tab URL.
|
|
114
|
+
*/
|
|
115
|
+
async getCurrentUrl(): Promise<string> {
|
|
116
|
+
try {
|
|
117
|
+
const result = await this.send("getTabInfo") as { url?: string };
|
|
118
|
+
return result.url ?? "";
|
|
119
|
+
} catch {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Disconnect from the extension.
|
|
126
|
+
*/
|
|
127
|
+
disconnect(): void {
|
|
128
|
+
this.rejectAllPending("Shutting down");
|
|
129
|
+
this.ws?.close();
|
|
130
|
+
this.ws = null;
|
|
131
|
+
this.connected = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
isConnected(): boolean {
|
|
135
|
+
return this.connected;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private handleMessage(raw: string): void {
|
|
139
|
+
try {
|
|
140
|
+
const msg = JSON.parse(raw) as {
|
|
141
|
+
id?: number;
|
|
142
|
+
method?: string;
|
|
143
|
+
params?: Record<string, unknown>;
|
|
144
|
+
result?: unknown;
|
|
145
|
+
error?: { message: string };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// CDP event (no id, has method) — dispatch to listeners
|
|
149
|
+
if (!msg.id && msg.method) {
|
|
150
|
+
const handlers = this.eventListeners.get(msg.method);
|
|
151
|
+
if (handlers) {
|
|
152
|
+
for (const handler of handlers) {
|
|
153
|
+
try {
|
|
154
|
+
handler(msg.params ?? {});
|
|
155
|
+
} catch {
|
|
156
|
+
// event handler error should not break the bridge
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// CDP response (has id)
|
|
164
|
+
if (msg.id && this.pendingRequests.has(msg.id)) {
|
|
165
|
+
const pending = this.pendingRequests.get(msg.id)!;
|
|
166
|
+
this.pendingRequests.delete(msg.id);
|
|
167
|
+
clearTimeout(pending.timeout);
|
|
168
|
+
|
|
169
|
+
if (msg.error) {
|
|
170
|
+
pending.reject(new Error(msg.error.message));
|
|
171
|
+
} else {
|
|
172
|
+
pending.resolve(msg.result);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
process.stderr.write(`[sensai:chrome] Failed to parse message: ${raw.slice(0, 200)}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private rejectAllPending(reason: string): void {
|
|
181
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
182
|
+
clearTimeout(pending.timeout);
|
|
183
|
+
pending.reject(new Error(reason));
|
|
184
|
+
this.pendingRequests.delete(id);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|