opendevbrowser 0.0.10
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/README.md +241 -0
- package/dist/chunk-R5VUZEUU.js +128 -0
- package/dist/chunk-R5VUZEUU.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +802 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3615 -0
- package/dist/index.js.map +1 -0
- package/dist/opendevbrowser.d.ts +5 -0
- package/dist/opendevbrowser.js +3615 -0
- package/dist/opendevbrowser.js.map +1 -0
- package/extension/dist/background.js +32 -0
- package/extension/dist/popup.js +150 -0
- package/extension/dist/popup.jsx +150 -0
- package/extension/dist/relay-settings.js +4 -0
- package/extension/dist/services/CDPRouter.js +176 -0
- package/extension/dist/services/ConnectionManager.js +301 -0
- package/extension/dist/services/RelayClient.js +73 -0
- package/extension/dist/services/TabManager.js +18 -0
- package/extension/dist/types.js +1 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +34 -0
- package/extension/popup.html +108 -0
- package/package.json +71 -0
- package/skills/AGENTS.md +80 -0
- package/skills/data-extraction/SKILL.md +136 -0
- package/skills/form-testing/SKILL.md +113 -0
- package/skills/login-automation/SKILL.md +98 -0
- package/skills/opendevbrowser-best-practices/SKILL.md +81 -0
- package/skills/opendevbrowser-continuity-ledger/SKILL.md +45 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { DEFAULT_PAIRING_ENABLED, DEFAULT_PAIRING_TOKEN, DEFAULT_RELAY_PORT } from "../relay-settings.js";
|
|
2
|
+
import { RelayClient } from "./RelayClient.js";
|
|
3
|
+
import { CDPRouter } from "./CDPRouter.js";
|
|
4
|
+
import { TabManager } from "./TabManager.js";
|
|
5
|
+
export class ConnectionManager {
|
|
6
|
+
status = "disconnected";
|
|
7
|
+
listeners = new Set();
|
|
8
|
+
relay = null;
|
|
9
|
+
cdp = new CDPRouter();
|
|
10
|
+
tabs = new TabManager();
|
|
11
|
+
trackedTab = null;
|
|
12
|
+
disconnecting = false;
|
|
13
|
+
shouldReconnect = false;
|
|
14
|
+
reconnectTimer = null;
|
|
15
|
+
reconnectAttempts = 0;
|
|
16
|
+
reconnectDelayMs = 500;
|
|
17
|
+
pairingToken = DEFAULT_PAIRING_TOKEN;
|
|
18
|
+
pairingEnabled = DEFAULT_PAIRING_ENABLED;
|
|
19
|
+
relayPort = DEFAULT_RELAY_PORT;
|
|
20
|
+
maxReconnectAttempts = 5;
|
|
21
|
+
maxReconnectDelayMs = 5000;
|
|
22
|
+
constructor() {
|
|
23
|
+
this.loadSettings().catch(() => { });
|
|
24
|
+
chrome.storage.onChanged.addListener(this.handleStorageChange);
|
|
25
|
+
chrome.tabs.onRemoved.addListener(this.handleTabRemoved);
|
|
26
|
+
chrome.tabs.onUpdated.addListener(this.handleTabUpdated);
|
|
27
|
+
}
|
|
28
|
+
getStatus() {
|
|
29
|
+
return this.status;
|
|
30
|
+
}
|
|
31
|
+
async connect() {
|
|
32
|
+
if (this.status === "connected") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
this.shouldReconnect = true;
|
|
37
|
+
this.reconnectAttempts = 0;
|
|
38
|
+
await this.loadSettings();
|
|
39
|
+
await this.attachToActiveTab();
|
|
40
|
+
await this.connectRelay();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
await this.disconnect();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async disconnect() {
|
|
47
|
+
if (this.disconnecting)
|
|
48
|
+
return;
|
|
49
|
+
this.disconnecting = true;
|
|
50
|
+
this.shouldReconnect = false;
|
|
51
|
+
this.clearReconnectTimer();
|
|
52
|
+
try {
|
|
53
|
+
if (this.relay) {
|
|
54
|
+
this.relay.disconnect();
|
|
55
|
+
this.relay = null;
|
|
56
|
+
}
|
|
57
|
+
if (this.trackedTab !== null) {
|
|
58
|
+
await this.cdp.detach();
|
|
59
|
+
this.trackedTab = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
this.disconnecting = false;
|
|
64
|
+
this.setStatus("disconnected");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
onStatus(listener) {
|
|
68
|
+
this.listeners.add(listener);
|
|
69
|
+
return () => this.listeners.delete(listener);
|
|
70
|
+
}
|
|
71
|
+
setStatus(status) {
|
|
72
|
+
this.status = status;
|
|
73
|
+
for (const listener of this.listeners) {
|
|
74
|
+
listener(status);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async attachToActiveTab() {
|
|
78
|
+
const tab = await this.tabs.getActiveTab();
|
|
79
|
+
if (!tab || typeof tab.id !== "number") {
|
|
80
|
+
this.trackedTab = null;
|
|
81
|
+
this.setStatus("disconnected");
|
|
82
|
+
throw new Error("No active tab available");
|
|
83
|
+
}
|
|
84
|
+
await this.cdp.attach(tab.id);
|
|
85
|
+
this.trackedTab = {
|
|
86
|
+
id: tab.id,
|
|
87
|
+
url: tab.url ?? undefined,
|
|
88
|
+
title: tab.title ?? undefined,
|
|
89
|
+
groupId: typeof tab.groupId === "number" ? tab.groupId : undefined
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async connectRelay() {
|
|
93
|
+
if (!this.trackedTab) {
|
|
94
|
+
throw new Error("No tracked tab for relay connection");
|
|
95
|
+
}
|
|
96
|
+
const relay = new RelayClient(this.buildRelayUrl(), {
|
|
97
|
+
onCommand: (command) => {
|
|
98
|
+
this.cdp.handleCommand(command).catch(() => {
|
|
99
|
+
this.disconnect().catch(() => { });
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
onClose: () => {
|
|
103
|
+
this.handleRelayClose();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
this.relay = relay;
|
|
107
|
+
this.cdp.setCallbacks({
|
|
108
|
+
onEvent: (event) => this.relay?.sendEvent(event),
|
|
109
|
+
onResponse: (response) => this.relay?.sendResponse(response),
|
|
110
|
+
onDetach: () => {
|
|
111
|
+
this.disconnect().catch(() => { });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
await relay.connect(this.buildHandshake());
|
|
116
|
+
this.setStatus("connected");
|
|
117
|
+
this.reconnectAttempts = 0;
|
|
118
|
+
this.reconnectDelayMs = 500;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (this.relay === relay) {
|
|
122
|
+
this.relay = null;
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
handleRelayClose() {
|
|
128
|
+
this.relay = null;
|
|
129
|
+
if (!this.shouldReconnect || !this.trackedTab) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.setStatus("disconnected");
|
|
133
|
+
this.scheduleReconnect();
|
|
134
|
+
}
|
|
135
|
+
scheduleReconnect() {
|
|
136
|
+
if (this.reconnectTimer !== null) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
140
|
+
this.disconnect().catch(() => { });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.reconnectTimer = setTimeout(() => {
|
|
144
|
+
this.reconnectTimer = null;
|
|
145
|
+
this.reconnectAttempts += 1;
|
|
146
|
+
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, this.maxReconnectDelayMs);
|
|
147
|
+
this.reconnectRelay().catch(() => {
|
|
148
|
+
this.scheduleReconnect();
|
|
149
|
+
});
|
|
150
|
+
}, this.reconnectDelayMs);
|
|
151
|
+
}
|
|
152
|
+
async reconnectRelay() {
|
|
153
|
+
if (!this.trackedTab || !this.shouldReconnect) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const attachedId = this.cdp.getAttachedTabId();
|
|
157
|
+
if (attachedId !== this.trackedTab.id) {
|
|
158
|
+
this.disconnect().catch(() => { });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const tab = await this.tabs.getTab(this.trackedTab.id);
|
|
162
|
+
if (!tab) {
|
|
163
|
+
this.disconnect().catch(() => { });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.trackedTab = {
|
|
167
|
+
id: tab.id ?? this.trackedTab.id,
|
|
168
|
+
url: tab.url ?? this.trackedTab.url,
|
|
169
|
+
title: tab.title ?? this.trackedTab.title,
|
|
170
|
+
groupId: typeof tab.groupId === "number" ? tab.groupId : this.trackedTab.groupId
|
|
171
|
+
};
|
|
172
|
+
await this.connectRelay();
|
|
173
|
+
}
|
|
174
|
+
buildHandshake() {
|
|
175
|
+
if (!this.trackedTab) {
|
|
176
|
+
throw new Error("No tracked tab for handshake");
|
|
177
|
+
}
|
|
178
|
+
const payload = {
|
|
179
|
+
tabId: this.trackedTab.id,
|
|
180
|
+
url: this.trackedTab.url,
|
|
181
|
+
title: this.trackedTab.title,
|
|
182
|
+
groupId: this.trackedTab.groupId
|
|
183
|
+
};
|
|
184
|
+
if (this.pairingEnabled && this.pairingToken) {
|
|
185
|
+
payload.pairingToken = this.pairingToken;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
type: "handshake",
|
|
189
|
+
payload
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
handleStorageChange = (changes, area) => {
|
|
193
|
+
if (area !== "local") {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (changes.pairingToken) {
|
|
197
|
+
this.updatePairingToken(changes.pairingToken.newValue);
|
|
198
|
+
this.refreshHandshake();
|
|
199
|
+
}
|
|
200
|
+
if (changes.pairingEnabled) {
|
|
201
|
+
this.updatePairingEnabled(changes.pairingEnabled.newValue);
|
|
202
|
+
this.ensurePairingTokenDefault();
|
|
203
|
+
this.refreshHandshake();
|
|
204
|
+
}
|
|
205
|
+
if (changes.relayPort) {
|
|
206
|
+
this.updateRelayPort(changes.relayPort.newValue);
|
|
207
|
+
this.refreshRelay().catch(() => { });
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
handleTabRemoved = (tabId) => {
|
|
211
|
+
if (this.trackedTab && this.trackedTab.id === tabId) {
|
|
212
|
+
this.disconnect().catch(() => { });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
handleTabUpdated = (_tabId, _changeInfo, tab) => {
|
|
216
|
+
if (!this.trackedTab || tab.id !== this.trackedTab.id) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.trackedTab = {
|
|
220
|
+
id: tab.id,
|
|
221
|
+
url: tab.url ?? this.trackedTab.url,
|
|
222
|
+
title: tab.title ?? this.trackedTab.title,
|
|
223
|
+
groupId: typeof tab.groupId === "number" ? tab.groupId : this.trackedTab.groupId
|
|
224
|
+
};
|
|
225
|
+
if (this.relay?.isConnected()) {
|
|
226
|
+
this.relay.sendHandshake(this.buildHandshake());
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
async loadSettings() {
|
|
230
|
+
const data = await new Promise((resolve) => {
|
|
231
|
+
chrome.storage.local.get(["pairingToken", "pairingEnabled", "relayPort"], (items) => {
|
|
232
|
+
resolve(items);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
this.updatePairingEnabled(data.pairingEnabled);
|
|
236
|
+
this.updatePairingToken(data.pairingToken);
|
|
237
|
+
this.updateRelayPort(data.relayPort);
|
|
238
|
+
this.ensurePairingTokenDefault();
|
|
239
|
+
}
|
|
240
|
+
updatePairingToken(value) {
|
|
241
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
242
|
+
this.pairingToken = value.trim();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.pairingToken = null;
|
|
246
|
+
}
|
|
247
|
+
updatePairingEnabled(value) {
|
|
248
|
+
if (typeof value === "boolean") {
|
|
249
|
+
this.pairingEnabled = value;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.pairingEnabled = DEFAULT_PAIRING_ENABLED;
|
|
253
|
+
}
|
|
254
|
+
ensurePairingTokenDefault() {
|
|
255
|
+
if (!this.pairingEnabled || this.pairingToken) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this.pairingToken = DEFAULT_PAIRING_TOKEN;
|
|
259
|
+
chrome.storage.local.set({ pairingToken: DEFAULT_PAIRING_TOKEN });
|
|
260
|
+
}
|
|
261
|
+
updateRelayPort(value) {
|
|
262
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) {
|
|
263
|
+
this.relayPort = value;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (typeof value === "string" && value.trim()) {
|
|
267
|
+
const parsed = Number(value);
|
|
268
|
+
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
|
|
269
|
+
this.relayPort = parsed;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this.relayPort = DEFAULT_RELAY_PORT;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Chrome automatically sends Origin: chrome-extension://EXTENSION_ID
|
|
277
|
+
* for WebSocket connections from extensions. The relay server validates
|
|
278
|
+
* this to prevent CSWSH attacks from web pages.
|
|
279
|
+
*/
|
|
280
|
+
buildRelayUrl() {
|
|
281
|
+
return `ws://127.0.0.1:${this.relayPort}/extension`;
|
|
282
|
+
}
|
|
283
|
+
async refreshRelay() {
|
|
284
|
+
if (this.status !== "connected")
|
|
285
|
+
return;
|
|
286
|
+
await this.disconnect();
|
|
287
|
+
await this.connect();
|
|
288
|
+
}
|
|
289
|
+
clearReconnectTimer() {
|
|
290
|
+
if (this.reconnectTimer !== null) {
|
|
291
|
+
clearTimeout(this.reconnectTimer);
|
|
292
|
+
this.reconnectTimer = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
refreshHandshake() {
|
|
296
|
+
if (!this.trackedTab || !this.relay?.isConnected()) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.relay.sendHandshake(this.buildHandshake());
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export class RelayClient {
|
|
2
|
+
url;
|
|
3
|
+
handlers;
|
|
4
|
+
socket = null;
|
|
5
|
+
constructor(url, handlers) {
|
|
6
|
+
this.url = url;
|
|
7
|
+
this.handlers = handlers;
|
|
8
|
+
}
|
|
9
|
+
async connect(handshake) {
|
|
10
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
this.socket = new WebSocket(this.url);
|
|
14
|
+
await new Promise((resolve, reject) => {
|
|
15
|
+
if (!this.socket) {
|
|
16
|
+
reject(new Error("Relay socket not created"));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.socket.addEventListener("open", () => resolve(), { once: true });
|
|
20
|
+
this.socket.addEventListener("error", () => reject(new Error("Relay socket error")), {
|
|
21
|
+
once: true
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
this.socket.addEventListener("message", (event) => {
|
|
25
|
+
const message = parseJson(event.data);
|
|
26
|
+
if (!message || typeof message !== "object")
|
|
27
|
+
return;
|
|
28
|
+
const record = message;
|
|
29
|
+
if (record.method === "forwardCDPCommand") {
|
|
30
|
+
this.handlers.onCommand(record);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
this.socket.addEventListener("close", () => {
|
|
34
|
+
this.handlers.onClose();
|
|
35
|
+
});
|
|
36
|
+
this.send(handshake);
|
|
37
|
+
}
|
|
38
|
+
disconnect() {
|
|
39
|
+
if (!this.socket)
|
|
40
|
+
return;
|
|
41
|
+
if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
|
|
42
|
+
this.socket.close(1000, "Relay disconnect");
|
|
43
|
+
}
|
|
44
|
+
this.socket = null;
|
|
45
|
+
}
|
|
46
|
+
sendResponse(response) {
|
|
47
|
+
this.send(response);
|
|
48
|
+
}
|
|
49
|
+
sendEvent(event) {
|
|
50
|
+
this.send(event);
|
|
51
|
+
}
|
|
52
|
+
sendHandshake(handshake) {
|
|
53
|
+
this.send(handshake);
|
|
54
|
+
}
|
|
55
|
+
isConnected() {
|
|
56
|
+
return Boolean(this.socket && this.socket.readyState === WebSocket.OPEN);
|
|
57
|
+
}
|
|
58
|
+
send(payload) {
|
|
59
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
|
|
60
|
+
return;
|
|
61
|
+
this.socket.send(JSON.stringify(payload));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const parseJson = (data) => {
|
|
65
|
+
if (typeof data !== "string")
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(data);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class TabManager {
|
|
2
|
+
async getTab(tabId) {
|
|
3
|
+
try {
|
|
4
|
+
return await chrome.tabs.get(tabId);
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
async getActiveTab() {
|
|
11
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
12
|
+
return tabs[0] ?? null;
|
|
13
|
+
}
|
|
14
|
+
async getActiveTabId() {
|
|
15
|
+
const tab = await this.getActiveTab();
|
|
16
|
+
return tab?.id ?? null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "OpenDevBrowser Relay",
|
|
4
|
+
"version": "0.0.10",
|
|
5
|
+
"description": "Optional bridge to reuse existing Chrome tabs with OpenDevBrowser.",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"debugger",
|
|
8
|
+
"tabs",
|
|
9
|
+
"storage"
|
|
10
|
+
],
|
|
11
|
+
"host_permissions": [
|
|
12
|
+
"http://127.0.0.1/*",
|
|
13
|
+
"http://localhost/*"
|
|
14
|
+
],
|
|
15
|
+
"icons": {
|
|
16
|
+
"16": "icons/icon16.png",
|
|
17
|
+
"32": "icons/icon32.png",
|
|
18
|
+
"48": "icons/icon48.png",
|
|
19
|
+
"128": "icons/icon128.png"
|
|
20
|
+
},
|
|
21
|
+
"action": {
|
|
22
|
+
"default_popup": "popup.html",
|
|
23
|
+
"default_icon": {
|
|
24
|
+
"16": "icons/icon16.png",
|
|
25
|
+
"32": "icons/icon32.png",
|
|
26
|
+
"48": "icons/icon48.png",
|
|
27
|
+
"128": "icons/icon128.png"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"background": {
|
|
31
|
+
"service_worker": "dist/background.js",
|
|
32
|
+
"type": "module"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>OpenDevBrowser</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: Arial, sans-serif;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 12px;
|
|
12
|
+
min-width: 280px;
|
|
13
|
+
}
|
|
14
|
+
h1 {
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
margin: 0 0 8px;
|
|
17
|
+
}
|
|
18
|
+
.status-container {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 8px;
|
|
22
|
+
margin-bottom: 12px;
|
|
23
|
+
}
|
|
24
|
+
.status-indicator {
|
|
25
|
+
width: 12px;
|
|
26
|
+
height: 12px;
|
|
27
|
+
border-radius: 50%;
|
|
28
|
+
background-color: #dc3545;
|
|
29
|
+
transition: background-color 0.3s ease;
|
|
30
|
+
}
|
|
31
|
+
.status-indicator.connected {
|
|
32
|
+
background-color: #28a745;
|
|
33
|
+
}
|
|
34
|
+
#status {
|
|
35
|
+
font-size: 12px;
|
|
36
|
+
}
|
|
37
|
+
label {
|
|
38
|
+
font-size: 11px;
|
|
39
|
+
display: block;
|
|
40
|
+
margin-bottom: 4px;
|
|
41
|
+
}
|
|
42
|
+
label.toggle {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 6px;
|
|
46
|
+
margin-bottom: 10px;
|
|
47
|
+
}
|
|
48
|
+
input {
|
|
49
|
+
width: 100%;
|
|
50
|
+
padding: 6px;
|
|
51
|
+
margin-bottom: 10px;
|
|
52
|
+
border: 1px solid #ccc;
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
}
|
|
56
|
+
input[type="checkbox"] {
|
|
57
|
+
width: auto;
|
|
58
|
+
margin: 0;
|
|
59
|
+
padding: 0;
|
|
60
|
+
}
|
|
61
|
+
input:disabled {
|
|
62
|
+
background-color: #f5f5f5;
|
|
63
|
+
color: #999;
|
|
64
|
+
}
|
|
65
|
+
button {
|
|
66
|
+
width: 100%;
|
|
67
|
+
padding: 8px;
|
|
68
|
+
border: 1px solid #222;
|
|
69
|
+
background: #111;
|
|
70
|
+
color: #fff;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
border-radius: 4px;
|
|
73
|
+
transition: background-color 0.2s ease;
|
|
74
|
+
}
|
|
75
|
+
button:hover {
|
|
76
|
+
background: #333;
|
|
77
|
+
}
|
|
78
|
+
.auto-pair-note {
|
|
79
|
+
font-size: 10px;
|
|
80
|
+
color: #666;
|
|
81
|
+
margin-top: -6px;
|
|
82
|
+
margin-bottom: 10px;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<h1>OpenDevBrowser</h1>
|
|
88
|
+
<div class="status-container">
|
|
89
|
+
<div id="statusIndicator" class="status-indicator"></div>
|
|
90
|
+
<div id="status">Disconnected</div>
|
|
91
|
+
</div>
|
|
92
|
+
<label for="relayPort">Relay port</label>
|
|
93
|
+
<input id="relayPort" type="number" min="1" max="65535" />
|
|
94
|
+
<label class="toggle" for="autoPair">
|
|
95
|
+
<input id="autoPair" type="checkbox" />
|
|
96
|
+
Auto-Pair (fetch token from plugin)
|
|
97
|
+
</label>
|
|
98
|
+
<div class="auto-pair-note">When enabled, token is fetched automatically from running plugin</div>
|
|
99
|
+
<label class="toggle" for="pairingEnabled">
|
|
100
|
+
<input id="pairingEnabled" type="checkbox" />
|
|
101
|
+
Require pairing token
|
|
102
|
+
</label>
|
|
103
|
+
<label for="pairingToken">Pairing token</label>
|
|
104
|
+
<input id="pairingToken" type="text" placeholder="Enter token or enable Auto-Pair" />
|
|
105
|
+
<button id="toggle">Connect</button>
|
|
106
|
+
<script type="module" src="dist/popup.js"></script>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opendevbrowser",
|
|
3
|
+
"version": "0.0.10",
|
|
4
|
+
"description": "OpenCode plugin for browser automation via CDP with snapshot-refs-actions workflow",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"opendevbrowser": "dist/cli/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"skills",
|
|
14
|
+
"extension/manifest.json",
|
|
15
|
+
"extension/popup.html",
|
|
16
|
+
"extension/dist",
|
|
17
|
+
"extension/icons"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"opencode",
|
|
21
|
+
"plugin",
|
|
22
|
+
"browser",
|
|
23
|
+
"automation",
|
|
24
|
+
"cdp",
|
|
25
|
+
"playwright",
|
|
26
|
+
"testing",
|
|
27
|
+
"web-scraping",
|
|
28
|
+
"chrome"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/freshtechbro/opendevbrowser.git"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup src/index.ts src/cli/index.ts --format esm --dts --clean --sourcemap && node --input-type=module -e \"import { copyFileSync, existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nconst dist = resolve('dist');\nconst pairs = [\n ['index.js', 'opendevbrowser.js'],\n ['index.js.map', 'opendevbrowser.js.map'],\n ['index.d.ts', 'opendevbrowser.d.ts'],\n ['index.d.ts.map', 'opendevbrowser.d.ts.map'],\n];\nfor (const [src, dst] of pairs) {\n const from = resolve(dist, src);\n const to = resolve(dist, dst);\n if (existsSync(from)) copyFileSync(from, to);\n}\"",
|
|
40
|
+
"dev": "tsup src/index.ts src/cli/index.ts --format esm --dts --watch",
|
|
41
|
+
"lint": "eslint \"{src,tests}/**/*.ts\"",
|
|
42
|
+
"test": "vitest run --coverage",
|
|
43
|
+
"extension:sync": "node scripts/sync-extension-version.mjs",
|
|
44
|
+
"extension:build": "npm run extension:sync && tsc -p extension/tsconfig.json",
|
|
45
|
+
"extension:pack": "cd extension && zip -r ../opendevbrowser-extension.zip manifest.json popup.html dist/ icons/",
|
|
46
|
+
"version:check": "node scripts/verify-versions.mjs",
|
|
47
|
+
"prepack": "npm run build && npm run extension:build"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@opencode-ai/plugin": "^1.0.203",
|
|
51
|
+
"@puppeteer/browsers": "^2.2.0",
|
|
52
|
+
"async-mutex": "^0.5.0",
|
|
53
|
+
"jsonc-parser": "^3.2.0",
|
|
54
|
+
"playwright-core": "^1.49.1",
|
|
55
|
+
"ws": "^8.17.1",
|
|
56
|
+
"zod": "^3.23.8"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/chrome": "^0.0.270",
|
|
60
|
+
"@types/node": "^20.19.27",
|
|
61
|
+
"@types/ws": "^8.18.1",
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "^8.9.0",
|
|
63
|
+
"@typescript-eslint/parser": "^8.9.0",
|
|
64
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
65
|
+
"eslint": "^9.12.0",
|
|
66
|
+
"happy-dom": "^20.0.11",
|
|
67
|
+
"tsup": "^8.5.1",
|
|
68
|
+
"typescript": "^5.9.3",
|
|
69
|
+
"vitest": "^4.0.16"
|
|
70
|
+
}
|
|
71
|
+
}
|
package/skills/AGENTS.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Local AGENTS.md (skills/)
|
|
2
|
+
|
|
3
|
+
Applies to `skills/` and subdirectories. Extends root `AGENTS.md`.
|
|
4
|
+
|
|
5
|
+
## Skill Pack Architecture
|
|
6
|
+
- Each skill pack lives in its own folder with `SKILL.md` as the entry point.
|
|
7
|
+
- `opendevbrowser-best-practices` is the canonical prompting guide source.
|
|
8
|
+
- OpenCode-native discovery is primary:
|
|
9
|
+
- Project-local: `.opencode/skill/*/SKILL.md`
|
|
10
|
+
- Global: `~/.config/opencode/skill/*/SKILL.md`
|
|
11
|
+
- Compatibility-only paths: `.claude/skills/*/SKILL.md`, `~/.claude/skills/*/SKILL.md`
|
|
12
|
+
- `opendevbrowser_skill_list/load` are compatibility wrappers; OpenCode `skill` is primary.
|
|
13
|
+
|
|
14
|
+
## Skill Pack Rules
|
|
15
|
+
- `skills/opendevbrowser-best-practices/SKILL.md` is the source for prompting guide output.
|
|
16
|
+
- Keep guidance short, script-first, and snapshot-first.
|
|
17
|
+
- Keep examples aligned with `opendevbrowser_*` tool names.
|
|
18
|
+
- Do not include secrets or captured page data in skill content.
|
|
19
|
+
|
|
20
|
+
## Skill Format Specification
|
|
21
|
+
|
|
22
|
+
### Naming Conventions (OpenCode alignment)
|
|
23
|
+
- Skill names: lowercase, hyphens only, 1-64 characters
|
|
24
|
+
- Directory name must match skill name in frontmatter
|
|
25
|
+
- Examples: `login-automation`, `form-testing`, `data-extraction`
|
|
26
|
+
|
|
27
|
+
### SKILL.md Structure
|
|
28
|
+
```markdown
|
|
29
|
+
---
|
|
30
|
+
name: skill-name
|
|
31
|
+
description: Brief description (1-1024 chars)
|
|
32
|
+
version: 1.0.0
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
# Skill Title
|
|
36
|
+
|
|
37
|
+
## Section Heading
|
|
38
|
+
Content organized by topic for filtering.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Required Frontmatter
|
|
42
|
+
| Field | Required | Description |
|
|
43
|
+
|-------|----------|-------------|
|
|
44
|
+
| `name` | Yes | Skill identifier (lowercase, hyphens) |
|
|
45
|
+
| `description` | Yes | Brief description for listing |
|
|
46
|
+
| `version` | No | Semantic version (defaults to 1.0.0) |
|
|
47
|
+
|
|
48
|
+
## Available Skills
|
|
49
|
+
|
|
50
|
+
| Skill | Purpose |
|
|
51
|
+
|-------|---------|
|
|
52
|
+
| `opendevbrowser-best-practices` | Core prompting guide for browser automation |
|
|
53
|
+
| `opendevbrowser-continuity-ledger` | Continuity ledger guidance for long-running tasks |
|
|
54
|
+
| `login-automation` | Authentication and credential handling |
|
|
55
|
+
| `form-testing` | Form validation and submission testing |
|
|
56
|
+
| `data-extraction` | Table extraction and pagination handling |
|
|
57
|
+
|
|
58
|
+
## Custom Skill Paths
|
|
59
|
+
Advanced: users can add custom search paths via `skillPaths` in `opendevbrowser.jsonc`:
|
|
60
|
+
```jsonc
|
|
61
|
+
{
|
|
62
|
+
"skillPaths": ["~/.config/opencode/opendevbrowser-skills"]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Folder Structure
|
|
67
|
+
```
|
|
68
|
+
skills/
|
|
69
|
+
|-- opendevbrowser-best-practices/
|
|
70
|
+
| `-- SKILL.md
|
|
71
|
+
|-- opendevbrowser-continuity-ledger/
|
|
72
|
+
| `-- SKILL.md
|
|
73
|
+
|-- login-automation/
|
|
74
|
+
| `-- SKILL.md
|
|
75
|
+
|-- form-testing/
|
|
76
|
+
| `-- SKILL.md
|
|
77
|
+
|-- data-extraction/
|
|
78
|
+
| `-- SKILL.md
|
|
79
|
+
`-- AGENTS.md
|
|
80
|
+
```
|