opendevbrowser 0.0.12 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -28
  3. package/dist/chunk-JVBMT2O5.js +7173 -0
  4. package/dist/chunk-JVBMT2O5.js.map +1 -0
  5. package/dist/cli/index.js +2486 -589
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1057 -194
  8. package/dist/index.js.map +1 -1
  9. package/dist/opendevbrowser.js +1057 -194
  10. package/dist/opendevbrowser.js.map +1 -1
  11. package/extension/dist/annotate-content.css +237 -0
  12. package/extension/dist/annotate-content.js +934 -0
  13. package/extension/dist/background.js +1194 -32
  14. package/extension/dist/logging.js +50 -0
  15. package/extension/dist/ops/dom-bridge.js +355 -0
  16. package/extension/dist/ops/ops-runtime.js +1249 -0
  17. package/extension/dist/ops/ops-session-store.js +189 -0
  18. package/extension/dist/ops/redaction.js +52 -0
  19. package/extension/dist/ops/snapshot-builder.js +4 -0
  20. package/extension/dist/ops/snapshot-shared.js +220 -0
  21. package/extension/dist/popup.js +370 -25
  22. package/extension/dist/relay-settings.js +1 -0
  23. package/extension/dist/services/CDPRouter.js +501 -103
  24. package/extension/dist/services/ConnectionManager.js +464 -57
  25. package/extension/dist/services/NativePortManager.js +182 -0
  26. package/extension/dist/services/RelayClient.js +227 -26
  27. package/extension/dist/services/TabManager.js +81 -0
  28. package/extension/dist/services/TargetSessionMap.js +146 -0
  29. package/extension/dist/services/cdp-router-commands.js +203 -0
  30. package/extension/dist/services/url-restrictions.js +41 -0
  31. package/extension/dist/types.js +3 -1
  32. package/extension/manifest.json +17 -3
  33. package/extension/popup.html +144 -0
  34. package/package.json +2 -2
  35. package/skills/AGENTS.md +34 -62
  36. package/skills/data-extraction/SKILL.md +95 -103
  37. package/skills/form-testing/SKILL.md +75 -82
  38. package/skills/login-automation/SKILL.md +76 -66
  39. package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
  40. package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
  41. package/dist/chunk-WTFSMBVH.js +0 -2815
  42. package/dist/chunk-WTFSMBVH.js.map +0 -1
  43. package/extension/dist/popup.jsx +0 -150
@@ -0,0 +1,182 @@
1
+ const DEFAULT_HOST = "com.opendevbrowser.native";
2
+ export class NativePortManager {
3
+ port = null;
4
+ status = "disconnected";
5
+ lastError = null;
6
+ lastPongAt = null;
7
+ queue = [];
8
+ pendingPing = null;
9
+ handlers;
10
+ hostName;
11
+ connectPromise = null;
12
+ constructor(handlers = {}, hostName = DEFAULT_HOST) {
13
+ this.handlers = handlers;
14
+ this.hostName = hostName;
15
+ }
16
+ isConnected() {
17
+ return this.status === "connected";
18
+ }
19
+ getHealth() {
20
+ return {
21
+ status: this.status,
22
+ error: this.lastError?.code,
23
+ detail: this.lastError?.message,
24
+ lastPongAt: this.lastPongAt ?? undefined
25
+ };
26
+ }
27
+ async connect() {
28
+ if (this.connectPromise) {
29
+ return await this.connectPromise;
30
+ }
31
+ const run = (async () => {
32
+ this.disconnect();
33
+ let port;
34
+ try {
35
+ port = chrome.runtime.connectNative(this.hostName);
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ this.setError(classifyNativeError(message), message);
40
+ return false;
41
+ }
42
+ this.port = port;
43
+ this.status = "connected";
44
+ this.lastError = null;
45
+ port.onMessage.addListener((payload) => this.handleMessage(payload));
46
+ port.onDisconnect.addListener(() => this.handleDisconnect());
47
+ this.flushQueue();
48
+ return true;
49
+ })();
50
+ this.connectPromise = run;
51
+ try {
52
+ return await run;
53
+ }
54
+ finally {
55
+ if (this.connectPromise === run) {
56
+ this.connectPromise = null;
57
+ }
58
+ }
59
+ }
60
+ disconnect() {
61
+ if (this.port) {
62
+ try {
63
+ this.port.disconnect();
64
+ }
65
+ catch {
66
+ // Ignore disconnect errors.
67
+ }
68
+ }
69
+ this.port = null;
70
+ this.status = "disconnected";
71
+ this.clearPendingPing();
72
+ }
73
+ send(payload) {
74
+ if (this.port && this.status === "connected") {
75
+ this.port.postMessage(payload);
76
+ return;
77
+ }
78
+ this.queue.push(payload);
79
+ }
80
+ async ping(timeoutMs = 5000) {
81
+ if (!this.port || this.status !== "connected") {
82
+ throw new Error("Native port not connected");
83
+ }
84
+ const id = crypto.randomUUID();
85
+ this.clearPendingPing();
86
+ return await new Promise((resolve, reject) => {
87
+ const timeoutId = setTimeout(() => {
88
+ this.setError("host_timeout", "Native host ping timed out");
89
+ reject(new Error("Native host ping timed out"));
90
+ }, timeoutMs);
91
+ this.pendingPing = { id, resolve, reject, timeoutId };
92
+ this.port?.postMessage({ type: "ping", id });
93
+ });
94
+ }
95
+ handleMessage(payload) {
96
+ if (!payload || typeof payload !== "object") {
97
+ return;
98
+ }
99
+ const record = payload;
100
+ if (record.type === "pong" && typeof record.id === "string") {
101
+ if (this.pendingPing && this.pendingPing.id === record.id) {
102
+ clearTimeout(this.pendingPing.timeoutId);
103
+ const resolve = this.pendingPing.resolve;
104
+ this.pendingPing = null;
105
+ this.lastPongAt = Date.now();
106
+ resolve();
107
+ }
108
+ return;
109
+ }
110
+ if (record.type === "error" && typeof record.code === "string" && typeof record.message === "string") {
111
+ const code = mapNativeErrorCode(record.code);
112
+ this.setError(code, record.message);
113
+ return;
114
+ }
115
+ this.handlers.onMessage?.(payload);
116
+ }
117
+ handleDisconnect() {
118
+ const lastError = chrome.runtime.lastError;
119
+ const message = lastError?.message ?? "Native host disconnected";
120
+ this.setError(classifyNativeError(message), message);
121
+ if (this.pendingPing) {
122
+ clearTimeout(this.pendingPing.timeoutId);
123
+ const reject = this.pendingPing.reject;
124
+ this.pendingPing = null;
125
+ reject(new Error(message));
126
+ }
127
+ this.port = null;
128
+ this.handlers.onDisconnect?.(this.lastError ?? undefined);
129
+ }
130
+ flushQueue() {
131
+ if (!this.port || this.status !== "connected")
132
+ return;
133
+ const queued = [...this.queue];
134
+ this.queue = [];
135
+ for (const payload of queued) {
136
+ this.port.postMessage(payload);
137
+ }
138
+ }
139
+ clearPendingPing() {
140
+ if (!this.pendingPing)
141
+ return;
142
+ clearTimeout(this.pendingPing.timeoutId);
143
+ this.pendingPing = null;
144
+ }
145
+ setError(code, message) {
146
+ this.status = code === "host_disconnect" ? "disconnected" : "error";
147
+ this.lastError = { code, message };
148
+ }
149
+ }
150
+ const classifyNativeError = (message) => {
151
+ const lowered = message.toLowerCase();
152
+ if (lowered.includes("not found")) {
153
+ return "host_not_installed";
154
+ }
155
+ if (lowered.includes("forbidden")) {
156
+ return "host_forbidden";
157
+ }
158
+ if (lowered.includes("exited") || lowered.includes("exit code")) {
159
+ return "host_disconnect";
160
+ }
161
+ if (lowered.includes("disconnect") || lowered.includes("disconnected") || lowered.includes("terminated")) {
162
+ return "host_disconnect";
163
+ }
164
+ return "unknown";
165
+ };
166
+ const mapNativeErrorCode = (code) => {
167
+ if (code === "host_message_too_large")
168
+ return "host_message_too_large";
169
+ if (code === "host_timeout")
170
+ return "host_timeout";
171
+ if (code === "host_forbidden")
172
+ return "host_forbidden";
173
+ if (code === "host_not_installed")
174
+ return "host_not_installed";
175
+ if (code === "host_disconnect")
176
+ return "host_disconnect";
177
+ return "unknown";
178
+ };
179
+ export const __test__ = {
180
+ classifyNativeError,
181
+ mapNativeErrorCode
182
+ };
@@ -1,39 +1,157 @@
1
+ import { logError } from "../logging.js";
1
2
  export class RelayClient {
2
3
  url;
3
4
  handlers;
4
5
  socket = null;
6
+ pendingHandshakeAckResolve = null;
7
+ pendingHandshakeAckReject = null;
8
+ pendingHandshakeAckTimeoutId = null;
9
+ lastHandshakeAck = null;
10
+ connectPromise = null;
11
+ pendingHealthChecks = new Map();
12
+ pendingPings = new Map();
5
13
  constructor(url, handlers) {
6
14
  this.url = url;
7
15
  this.handlers = handlers;
8
16
  }
9
17
  async connect(handshake) {
10
- if (this.socket && this.socket.readyState === WebSocket.OPEN) {
11
- return;
18
+ if (this.connectPromise) {
19
+ return await this.connectPromise;
12
20
  }
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;
21
+ const run = (async () => {
22
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
23
+ if (this.lastHandshakeAck) {
24
+ return this.lastHandshakeAck;
25
+ }
26
+ }
27
+ else {
28
+ this.socket = new WebSocket(this.url);
29
+ await new Promise((resolve, reject) => {
30
+ if (!this.socket) {
31
+ reject(new Error("Relay socket not created"));
32
+ return;
33
+ }
34
+ this.socket.addEventListener("open", () => resolve(), { once: true });
35
+ this.socket.addEventListener("error", () => reject(new Error("Relay socket error")), {
36
+ once: true
37
+ });
38
+ });
39
+ this.socket.addEventListener("message", (event) => {
40
+ const message = parseJson(event.data);
41
+ if (!message || typeof message !== "object")
42
+ return;
43
+ const record = message;
44
+ if (record.type === "handshakeAck") {
45
+ if (!isValidHandshakeAck(record)) {
46
+ if (this.pendingHandshakeAckReject) {
47
+ const reject = this.pendingHandshakeAckReject;
48
+ this.clearHandshakeAckWait();
49
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
50
+ this.socket.close(1002, "Invalid handshake acknowledgment");
51
+ }
52
+ reject(new Error("Relay handshake acknowledgement invalid"));
53
+ }
54
+ return;
55
+ }
56
+ const ack = record;
57
+ this.lastHandshakeAck = ack;
58
+ if (this.pendingHandshakeAckResolve) {
59
+ const resolve = this.pendingHandshakeAckResolve;
60
+ this.clearHandshakeAckWait();
61
+ resolve(ack);
62
+ }
63
+ return;
64
+ }
65
+ if (record.type === "healthCheckResult") {
66
+ if (!isValidHealthResponse(record)) {
67
+ return;
68
+ }
69
+ const pending = this.pendingHealthChecks.get(record.id);
70
+ if (pending) {
71
+ clearTimeout(pending.timeoutId);
72
+ this.pendingHealthChecks.delete(record.id);
73
+ pending.resolve(record.payload);
74
+ }
75
+ return;
76
+ }
77
+ if (record.type === "pong") {
78
+ if (!isValidPong(record)) {
79
+ return;
80
+ }
81
+ const pending = this.pendingPings.get(record.id);
82
+ if (pending) {
83
+ clearTimeout(pending.timeoutId);
84
+ this.pendingPings.delete(record.id);
85
+ pending.resolve(record.payload);
86
+ }
87
+ return;
88
+ }
89
+ if (record.method === "forwardCDPCommand") {
90
+ this.handlers.onCommand(record);
91
+ return;
92
+ }
93
+ if (record.type === "annotationCommand") {
94
+ this.handlers.onAnnotationCommand?.(record);
95
+ return;
96
+ }
97
+ if (isOpsEnvelope(record)) {
98
+ this.handlers.onOpsMessage?.(record);
99
+ return;
100
+ }
101
+ });
102
+ this.socket.addEventListener("close", (event) => {
103
+ if (this.pendingHandshakeAckReject) {
104
+ const reject = this.pendingHandshakeAckReject;
105
+ this.clearHandshakeAckWait();
106
+ reject(new Error("Relay socket closed before handshake acknowledgment"));
107
+ }
108
+ this.lastHandshakeAck = null;
109
+ for (const pending of this.pendingHealthChecks.values()) {
110
+ clearTimeout(pending.timeoutId);
111
+ pending.reject(new Error("Relay socket closed"));
112
+ }
113
+ this.pendingHealthChecks.clear();
114
+ for (const pending of this.pendingPings.values()) {
115
+ clearTimeout(pending.timeoutId);
116
+ pending.reject(new Error("Relay socket closed"));
117
+ }
118
+ this.pendingPings.clear();
119
+ this.handlers.onClose({ code: event.code, reason: event.reason });
120
+ });
18
121
  }
19
- this.socket.addEventListener("open", () => resolve(), { once: true });
20
- this.socket.addEventListener("error", () => reject(new Error("Relay socket error")), {
21
- once: true
122
+ const ackPromise = new Promise((resolve, reject) => {
123
+ this.clearHandshakeAckWait();
124
+ this.pendingHandshakeAckResolve = resolve;
125
+ this.pendingHandshakeAckReject = reject;
126
+ this.pendingHandshakeAckTimeoutId = setTimeout(() => {
127
+ this.clearHandshakeAckWait();
128
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
129
+ this.socket.close(1000, "Handshake ack timeout");
130
+ }
131
+ reject(new Error("Relay handshake not acknowledged"));
132
+ }, 2000);
22
133
  });
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);
134
+ try {
135
+ this.send(handshake);
31
136
  }
32
- });
33
- this.socket.addEventListener("close", () => {
34
- this.handlers.onClose();
35
- });
36
- this.send(handshake);
137
+ catch (error) {
138
+ this.clearHandshakeAckWait();
139
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
140
+ this.socket.close(1000, "Handshake send failed");
141
+ }
142
+ throw error;
143
+ }
144
+ return await ackPromise;
145
+ })();
146
+ this.connectPromise = run;
147
+ try {
148
+ return await run;
149
+ }
150
+ finally {
151
+ if (this.connectPromise === run) {
152
+ this.connectPromise = null;
153
+ }
154
+ }
37
155
  }
38
156
  disconnect() {
39
157
  if (!this.socket)
@@ -42,6 +160,8 @@ export class RelayClient {
42
160
  this.socket.close(1000, "Relay disconnect");
43
161
  }
44
162
  this.socket = null;
163
+ this.lastHandshakeAck = null;
164
+ this.clearHandshakeAckWait();
45
165
  }
46
166
  sendResponse(response) {
47
167
  this.send(response);
@@ -52,14 +172,60 @@ export class RelayClient {
52
172
  sendHandshake(handshake) {
53
173
  this.send(handshake);
54
174
  }
175
+ sendAnnotationResponse(response) {
176
+ this.send(response);
177
+ }
178
+ sendAnnotationEvent(event) {
179
+ this.send(event);
180
+ }
181
+ sendOpsMessage(message) {
182
+ this.send(message);
183
+ }
184
+ async sendHealthCheck(timeoutMs = 1500) {
185
+ return await this.sendPing(timeoutMs);
186
+ }
187
+ async sendPing(timeoutMs = 1500) {
188
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
189
+ throw new Error("Relay socket not connected");
190
+ }
191
+ const id = crypto.randomUUID();
192
+ const request = { type: "ping", id };
193
+ return await new Promise((resolve, reject) => {
194
+ const timeoutId = setTimeout(() => {
195
+ this.pendingPings.delete(id);
196
+ reject(new Error("Relay ping timed out"));
197
+ }, timeoutMs);
198
+ this.pendingPings.set(id, { resolve, reject, timeoutId });
199
+ try {
200
+ this.send(request);
201
+ }
202
+ catch (error) {
203
+ clearTimeout(timeoutId);
204
+ this.pendingPings.delete(id);
205
+ reject(error instanceof Error ? error : new Error("Relay ping failed"));
206
+ }
207
+ });
208
+ }
55
209
  isConnected() {
56
210
  return Boolean(this.socket && this.socket.readyState === WebSocket.OPEN);
57
211
  }
212
+ getLastHandshakeAck() {
213
+ return this.lastHandshakeAck;
214
+ }
58
215
  send(payload) {
59
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
60
- return;
216
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
217
+ throw new Error("Relay socket not connected");
218
+ }
61
219
  this.socket.send(JSON.stringify(payload));
62
220
  }
221
+ clearHandshakeAckWait() {
222
+ if (this.pendingHandshakeAckTimeoutId !== null) {
223
+ clearTimeout(this.pendingHandshakeAckTimeoutId);
224
+ }
225
+ this.pendingHandshakeAckTimeoutId = null;
226
+ this.pendingHandshakeAckResolve = null;
227
+ this.pendingHandshakeAckReject = null;
228
+ }
63
229
  }
64
230
  const parseJson = (data) => {
65
231
  if (typeof data !== "string")
@@ -67,7 +233,42 @@ const parseJson = (data) => {
67
233
  try {
68
234
  return JSON.parse(data);
69
235
  }
70
- catch {
236
+ catch (error) {
237
+ logError("relay.parse_json", error, { code: "relay_parse_failed" });
71
238
  return null;
72
239
  }
73
240
  };
241
+ const isValidHandshakeAck = (value) => {
242
+ if (value.type !== "handshakeAck")
243
+ return false;
244
+ const payload = value.payload;
245
+ if (!payload || typeof payload !== "object")
246
+ return false;
247
+ const record = payload;
248
+ return typeof record.instanceId === "string" && typeof record.relayPort === "number";
249
+ };
250
+ const isValidHealthResponse = (value) => {
251
+ if (value.type !== "healthCheckResult")
252
+ return false;
253
+ if (typeof value.id !== "string")
254
+ return false;
255
+ const payload = value.payload;
256
+ if (!payload || typeof payload !== "object")
257
+ return false;
258
+ const record = payload;
259
+ return typeof record.reason === "string";
260
+ };
261
+ const isValidPong = (value) => {
262
+ if (value.type !== "pong")
263
+ return false;
264
+ if (typeof value.id !== "string")
265
+ return false;
266
+ const payload = value.payload;
267
+ if (!payload || typeof payload !== "object")
268
+ return false;
269
+ const record = payload;
270
+ return typeof record.reason === "string";
271
+ };
272
+ const isOpsEnvelope = (value) => {
273
+ return typeof value.type === "string" && value.type.startsWith("ops_");
274
+ };
@@ -1,4 +1,74 @@
1
1
  export class TabManager {
2
+ async createTab(url, active = true) {
3
+ return await new Promise((resolve, reject) => {
4
+ chrome.tabs.create({ url, active }, (tab) => {
5
+ const lastError = chrome.runtime.lastError;
6
+ if (lastError) {
7
+ reject(new Error(lastError.message));
8
+ return;
9
+ }
10
+ if (!tab) {
11
+ reject(new Error("Tab creation failed"));
12
+ return;
13
+ }
14
+ resolve(tab);
15
+ });
16
+ });
17
+ }
18
+ async waitForTabComplete(tabId, timeoutMs = 10000) {
19
+ const existing = await this.getTab(tabId);
20
+ if (existing?.status === "complete") {
21
+ return;
22
+ }
23
+ await new Promise((resolve, reject) => {
24
+ let settled = false;
25
+ const timeoutId = setTimeout(() => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ chrome.tabs.onUpdated.removeListener(listener);
30
+ reject(new Error("Tab load timeout"));
31
+ }, timeoutMs);
32
+ const listener = (updatedId, changeInfo) => {
33
+ if (updatedId !== tabId) {
34
+ return;
35
+ }
36
+ if (changeInfo.status === "complete") {
37
+ if (settled)
38
+ return;
39
+ settled = true;
40
+ clearTimeout(timeoutId);
41
+ chrome.tabs.onUpdated.removeListener(listener);
42
+ resolve();
43
+ }
44
+ };
45
+ chrome.tabs.onUpdated.addListener(listener);
46
+ });
47
+ }
48
+ async closeTab(tabId) {
49
+ await new Promise((resolve, reject) => {
50
+ chrome.tabs.remove(tabId, () => {
51
+ const lastError = chrome.runtime.lastError;
52
+ if (lastError) {
53
+ reject(new Error(lastError.message));
54
+ return;
55
+ }
56
+ resolve();
57
+ });
58
+ });
59
+ }
60
+ async activateTab(tabId) {
61
+ return await new Promise((resolve, reject) => {
62
+ chrome.tabs.update(tabId, { active: true }, (tab) => {
63
+ const lastError = chrome.runtime.lastError;
64
+ if (lastError) {
65
+ reject(new Error(lastError.message));
66
+ return;
67
+ }
68
+ resolve(tab ?? null);
69
+ });
70
+ });
71
+ }
2
72
  async getTab(tabId) {
3
73
  try {
4
74
  return await chrome.tabs.get(tabId);
@@ -15,4 +85,15 @@ export class TabManager {
15
85
  const tab = await this.getActiveTab();
16
86
  return tab?.id ?? null;
17
87
  }
88
+ async getFirstHttpTabId() {
89
+ const tabs = await chrome.tabs.query({});
90
+ const match = tabs.find((tab) => {
91
+ if (typeof tab.id !== "number")
92
+ return false;
93
+ if (!tab.url)
94
+ return false;
95
+ return tab.url.startsWith("http://") || tab.url.startsWith("https://");
96
+ });
97
+ return match?.id ?? null;
98
+ }
18
99
  }