opendevbrowser 0.0.11 → 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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -28
  3. package/dist/chunk-JVBMT2O5.js +7173 -0
  4. package/dist/chunk-JVBMT2O5.js.map +1 -0
  5. package/dist/cli/index.js +3690 -275
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1080 -2857
  8. package/dist/index.js.map +1 -1
  9. package/dist/opendevbrowser.js +1080 -2857
  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 +1291 -8
  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 +398 -21
  22. package/extension/dist/relay-settings.js +3 -1
  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/icons/icon128.png +0 -0
  33. package/extension/icons/icon16.png +0 -0
  34. package/extension/icons/icon32.png +0 -0
  35. package/extension/icons/icon48.png +0 -0
  36. package/extension/manifest.json +17 -3
  37. package/extension/popup.html +469 -65
  38. package/package.json +2 -2
  39. package/skills/AGENTS.md +34 -61
  40. package/skills/data-extraction/SKILL.md +95 -103
  41. package/skills/form-testing/SKILL.md +75 -82
  42. package/skills/login-automation/SKILL.md +76 -66
  43. package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
  44. package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
  45. package/dist/chunk-R5VUZEUU.js +0 -128
  46. package/dist/chunk-R5VUZEUU.js.map +0 -1
  47. package/extension/dist/popup.jsx +0 -150
@@ -0,0 +1,189 @@
1
+ export class OpsRefStore {
2
+ refsByTarget = new Map();
3
+ snapshotByTarget = new Map();
4
+ setSnapshot(targetId, entries) {
5
+ const map = new Map();
6
+ for (const entry of entries) {
7
+ map.set(entry.ref, entry);
8
+ }
9
+ const snapshotId = createId();
10
+ this.refsByTarget.set(targetId, map);
11
+ this.snapshotByTarget.set(targetId, snapshotId);
12
+ return { snapshotId, targetId, count: entries.length };
13
+ }
14
+ resolve(targetId, ref) {
15
+ const map = this.refsByTarget.get(targetId);
16
+ if (!map)
17
+ return null;
18
+ return map.get(ref) ?? null;
19
+ }
20
+ getSnapshotId(targetId) {
21
+ return this.snapshotByTarget.get(targetId) ?? null;
22
+ }
23
+ getRefCount(targetId) {
24
+ const map = this.refsByTarget.get(targetId);
25
+ return map ? map.size : 0;
26
+ }
27
+ clearTarget(targetId) {
28
+ this.refsByTarget.delete(targetId);
29
+ this.snapshotByTarget.delete(targetId);
30
+ }
31
+ }
32
+ export class OpsSessionStore {
33
+ sessions = new Map();
34
+ tabToSession = new Map();
35
+ createSession(ownerClientId, tabId, leaseId, info) {
36
+ const id = createId();
37
+ const targetId = `tab-${tabId}`;
38
+ const target = {
39
+ targetId,
40
+ tabId,
41
+ url: info?.url,
42
+ title: info?.title
43
+ };
44
+ const session = {
45
+ id,
46
+ ownerClientId,
47
+ leaseId,
48
+ state: "active",
49
+ tabId,
50
+ targetId,
51
+ activeTargetId: targetId,
52
+ createdAt: Date.now(),
53
+ lastUsedAt: Date.now(),
54
+ targets: new Map([[targetId, target]]),
55
+ nameToTarget: new Map(),
56
+ targetToName: new Map(),
57
+ refStore: new OpsRefStore(),
58
+ consoleEvents: [],
59
+ networkEvents: [],
60
+ networkRequests: new Map(),
61
+ consoleSeq: 0,
62
+ networkSeq: 0,
63
+ queue: Promise.resolve()
64
+ };
65
+ this.sessions.set(id, session);
66
+ this.tabToSession.set(tabId, id);
67
+ return session;
68
+ }
69
+ get(sessionId) {
70
+ return this.sessions.get(sessionId) ?? null;
71
+ }
72
+ getByTabId(tabId) {
73
+ const id = this.tabToSession.get(tabId);
74
+ if (!id)
75
+ return null;
76
+ return this.sessions.get(id) ?? null;
77
+ }
78
+ listOwnedBy(clientId) {
79
+ return Array.from(this.sessions.values()).filter((session) => session.ownerClientId === clientId);
80
+ }
81
+ delete(sessionId) {
82
+ const session = this.sessions.get(sessionId) ?? null;
83
+ if (!session)
84
+ return null;
85
+ this.sessions.delete(sessionId);
86
+ for (const target of session.targets.values()) {
87
+ this.tabToSession.delete(target.tabId);
88
+ }
89
+ return session;
90
+ }
91
+ addTarget(sessionId, tabId, info) {
92
+ const session = this.requireSession(sessionId);
93
+ const targetId = `tab-${tabId}`;
94
+ const target = {
95
+ targetId,
96
+ tabId,
97
+ url: info?.url,
98
+ title: info?.title
99
+ };
100
+ session.targets.set(targetId, target);
101
+ this.tabToSession.set(tabId, sessionId);
102
+ if (!session.activeTargetId) {
103
+ session.activeTargetId = targetId;
104
+ }
105
+ return target;
106
+ }
107
+ removeTarget(sessionId, targetId) {
108
+ const session = this.requireSession(sessionId);
109
+ const target = session.targets.get(targetId) ?? null;
110
+ if (!target)
111
+ return null;
112
+ session.targets.delete(targetId);
113
+ this.tabToSession.delete(target.tabId);
114
+ const name = session.targetToName.get(targetId);
115
+ if (name) {
116
+ session.targetToName.delete(targetId);
117
+ session.nameToTarget.delete(name);
118
+ }
119
+ if (session.activeTargetId === targetId) {
120
+ const [first] = session.targets.keys();
121
+ session.activeTargetId = first ?? "";
122
+ }
123
+ session.refStore.clearTarget(targetId);
124
+ return target;
125
+ }
126
+ getTargetIdByTabId(sessionId, tabId) {
127
+ const session = this.requireSession(sessionId);
128
+ for (const target of session.targets.values()) {
129
+ if (target.tabId === tabId) {
130
+ return target.targetId;
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ removeTargetByTabId(sessionId, tabId) {
136
+ const targetId = this.getTargetIdByTabId(sessionId, tabId);
137
+ if (!targetId)
138
+ return null;
139
+ return this.removeTarget(sessionId, targetId);
140
+ }
141
+ setActiveTarget(sessionId, targetId) {
142
+ const session = this.requireSession(sessionId);
143
+ if (!session.targets.has(targetId)) {
144
+ throw new Error(`Unknown targetId: ${targetId}`);
145
+ }
146
+ session.activeTargetId = targetId;
147
+ }
148
+ setName(sessionId, targetId, name) {
149
+ const session = this.requireSession(sessionId);
150
+ const trimmed = name.trim();
151
+ if (!trimmed) {
152
+ throw new Error("Name must be non-empty");
153
+ }
154
+ if (!session.targets.has(targetId)) {
155
+ throw new Error(`Unknown targetId: ${targetId}`);
156
+ }
157
+ const existing = session.nameToTarget.get(trimmed);
158
+ if (existing && existing !== targetId) {
159
+ throw new Error(`Name already in use: ${trimmed}`);
160
+ }
161
+ const previousName = session.targetToName.get(targetId);
162
+ if (previousName && previousName !== trimmed) {
163
+ session.nameToTarget.delete(previousName);
164
+ }
165
+ session.nameToTarget.set(trimmed, targetId);
166
+ session.targetToName.set(targetId, trimmed);
167
+ }
168
+ getTargetIdByName(sessionId, name) {
169
+ const session = this.requireSession(sessionId);
170
+ return session.nameToTarget.get(name.trim()) ?? null;
171
+ }
172
+ listNamedTargets(sessionId) {
173
+ const session = this.requireSession(sessionId);
174
+ return Array.from(session.nameToTarget.entries()).map(([name, targetId]) => ({ name, targetId }));
175
+ }
176
+ requireSession(sessionId) {
177
+ const session = this.sessions.get(sessionId);
178
+ if (!session) {
179
+ throw new Error(`Unknown sessionId: ${sessionId}`);
180
+ }
181
+ return session;
182
+ }
183
+ }
184
+ const createId = () => {
185
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
186
+ return crypto.randomUUID();
187
+ }
188
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
189
+ };
@@ -0,0 +1,52 @@
1
+ const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
2
+ const TOKEN_LIKE_PATTERN = /\b[A-Za-z0-9_-]{16,}\b/g;
3
+ const API_KEY_PREFIX_PATTERN = /\b(sk_|pk_|api_|key_|token_|secret_|bearer_)[A-Za-z0-9_-]+\b/gi;
4
+ const SENSITIVE_KV_PATTERN = /\b(token|key|secret|password|auth|bearer|credential)[=:]\s*\S+/gi;
5
+ const shouldRedactToken = (token) => {
6
+ if (/^(sk_|pk_|api_|key_|token_|secret_|bearer_)/i.test(token)) {
7
+ return true;
8
+ }
9
+ const categories = [
10
+ /[a-z]/.test(token),
11
+ /[A-Z]/.test(token),
12
+ /\d/.test(token),
13
+ /[_-]/.test(token)
14
+ ].filter(Boolean).length;
15
+ return categories >= 2;
16
+ };
17
+ export const redactConsoleText = (text) => {
18
+ let result = text.replace(SENSITIVE_KV_PATTERN, (match) => {
19
+ const sepIndex = match.search(/[=:]/);
20
+ return match.slice(0, sepIndex + 1) + "[REDACTED]";
21
+ });
22
+ result = result.replace(JWT_PATTERN, "[REDACTED]");
23
+ result = result.replace(API_KEY_PREFIX_PATTERN, "[REDACTED]");
24
+ result = result.replace(TOKEN_LIKE_PATTERN, (match) => (shouldRedactToken(match) ? "[REDACTED]" : match));
25
+ return result;
26
+ };
27
+ const shouldRedactPathSegment = (segment) => {
28
+ if (segment.length < 16)
29
+ return false;
30
+ if (/^\d+$/.test(segment))
31
+ return false;
32
+ if (/^[a-f0-9-]{36}$/i.test(segment))
33
+ return false;
34
+ if (/^(sk_|pk_|api_|key_|token_|secret_|bearer_)/i.test(segment))
35
+ return true;
36
+ const categories = [/[a-z]/, /[A-Z]/, /\d/, /[_-]/].filter(r => r.test(segment)).length;
37
+ return categories >= 3 && segment.length >= 20;
38
+ };
39
+ export const redactUrl = (rawUrl) => {
40
+ try {
41
+ const parsed = new URL(rawUrl);
42
+ parsed.search = "";
43
+ parsed.hash = "";
44
+ const segments = parsed.pathname.split("/");
45
+ const redactedSegments = segments.map(segment => shouldRedactPathSegment(segment) ? "[REDACTED]" : segment);
46
+ parsed.pathname = redactedSegments.join("/");
47
+ return parsed.toString();
48
+ }
49
+ catch {
50
+ return rawUrl.split(/[?#]/)[0] || rawUrl;
51
+ }
52
+ };
@@ -0,0 +1,4 @@
1
+ import { buildSnapshotFromCdp } from "./snapshot-shared.js";
2
+ export async function buildSnapshot(send, mode, mainFrameOnly = true, maxNodes) {
3
+ return await buildSnapshotFromCdp(send, mode, mainFrameOnly, maxNodes);
4
+ }
@@ -0,0 +1,220 @@
1
+ const DEFAULT_MAX_AX_NODES = 1000;
2
+ const ACTIONABLE_ROLES = new Set([
3
+ "button",
4
+ "link",
5
+ "textbox",
6
+ "searchbox",
7
+ "textarea",
8
+ "checkbox",
9
+ "radio",
10
+ "combobox",
11
+ "listbox",
12
+ "menuitem",
13
+ "menuitemcheckbox",
14
+ "menuitemradio",
15
+ "option",
16
+ "switch",
17
+ "tab",
18
+ "slider",
19
+ "spinbutton",
20
+ "treeitem"
21
+ ]);
22
+ const SEMANTIC_ROLES = new Set([
23
+ "heading",
24
+ "article",
25
+ "main",
26
+ "navigation",
27
+ "region",
28
+ "section",
29
+ "form",
30
+ "list",
31
+ "listitem",
32
+ "paragraph",
33
+ "img",
34
+ "table",
35
+ "row",
36
+ "cell",
37
+ "columnheader",
38
+ "rowheader",
39
+ "banner",
40
+ "contentinfo",
41
+ "complementary"
42
+ ]);
43
+ export const selectorFunction = function () {
44
+ if (!(this instanceof Element))
45
+ return null;
46
+ const escape = (value) => {
47
+ if (typeof CSS !== "undefined" && CSS.escape) {
48
+ return CSS.escape(value);
49
+ }
50
+ return String(value).replace(/([^\w-])/g, "\\$1");
51
+ };
52
+ const testId = this.getAttribute("data-testid");
53
+ if (testId) {
54
+ return '[data-testid="' + escape(testId) + '"]';
55
+ }
56
+ const ariaLabel = this.getAttribute("aria-label");
57
+ if (ariaLabel && ariaLabel.length < 50) {
58
+ return '[aria-label="' + escape(ariaLabel) + '"]';
59
+ }
60
+ const buildPathSelector = (start) => {
61
+ const parts = [];
62
+ let current = start;
63
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
64
+ let selector = current.nodeName.toLowerCase();
65
+ if (current.id) {
66
+ selector += "#" + escape(current.id);
67
+ parts.unshift(selector);
68
+ break;
69
+ }
70
+ const parentEl = current.parentElement;
71
+ if (!parentEl) {
72
+ parts.unshift(selector);
73
+ break;
74
+ }
75
+ let index = 1;
76
+ let sibling = current;
77
+ while (sibling && sibling.previousElementSibling) {
78
+ sibling = sibling.previousElementSibling;
79
+ index += 1;
80
+ }
81
+ selector += ":nth-child(" + index + ")";
82
+ parts.unshift(selector);
83
+ current = parentEl;
84
+ }
85
+ return parts.join(" > ");
86
+ };
87
+ return buildPathSelector(this);
88
+ };
89
+ const SELECTOR_FUNCTION = selectorFunction.toString();
90
+ export async function buildSnapshotFromCdp(send, mode, mainFrameOnly = true, maxNodes) {
91
+ await send("Accessibility.enable", {});
92
+ await send("DOM.enable", {});
93
+ const result = await send("Accessibility.getFullAXTree", {});
94
+ const nodes = Array.isArray(result.nodes) ? result.nodes : [];
95
+ const entries = [];
96
+ const lines = [];
97
+ const warnings = [];
98
+ const maxEntries = typeof maxNodes === "number" ? maxNodes : DEFAULT_MAX_AX_NODES;
99
+ let skippedFrameCount = 0;
100
+ for (const node of nodes) {
101
+ if (entries.length >= maxEntries)
102
+ break;
103
+ if (node.ignored)
104
+ continue;
105
+ if (typeof node.backendDOMNodeId !== "number")
106
+ continue;
107
+ if (mainFrameOnly && node.frameId) {
108
+ skippedFrameCount += 1;
109
+ continue;
110
+ }
111
+ const role = extractValue(node.role) || extractValue(node.chromeRole);
112
+ if (!role)
113
+ continue;
114
+ if (!shouldInclude(role, mode))
115
+ continue;
116
+ const selector = await resolveSelector(send, node.backendDOMNodeId);
117
+ if (!selector)
118
+ continue;
119
+ const ref = `r${entries.length + 1}`;
120
+ const name = redactText(extractValue(node.name));
121
+ const value = redactText(extractValue(node.value));
122
+ const disabled = isTruthyProperty(node.properties, "disabled");
123
+ const checked = isTruthyProperty(node.properties, "checked");
124
+ entries.push({
125
+ ref,
126
+ selector,
127
+ backendNodeId: node.backendDOMNodeId,
128
+ frameId: node.frameId,
129
+ role,
130
+ name
131
+ });
132
+ lines.push(formatNode({
133
+ ref,
134
+ role,
135
+ name,
136
+ value,
137
+ disabled,
138
+ checked
139
+ }));
140
+ }
141
+ if (mainFrameOnly && skippedFrameCount > 0) {
142
+ warnings.push(`Skipped ${skippedFrameCount} iframe nodes; snapshot limited to main frame.`);
143
+ }
144
+ return { entries, lines, warnings };
145
+ }
146
+ async function resolveSelector(send, backendNodeId) {
147
+ const resolved = await send("DOM.resolveNode", { backendNodeId });
148
+ const objectId = resolved.object?.objectId;
149
+ if (!objectId)
150
+ return null;
151
+ const result = await send("Runtime.callFunctionOn", {
152
+ objectId,
153
+ functionDeclaration: SELECTOR_FUNCTION,
154
+ returnByValue: true
155
+ });
156
+ const selector = result.result?.value;
157
+ if (typeof selector !== "string" || selector.trim().length === 0) {
158
+ return null;
159
+ }
160
+ return selector;
161
+ }
162
+ function shouldInclude(role, mode) {
163
+ const normalized = role.toLowerCase();
164
+ if (ACTIONABLE_ROLES.has(normalized))
165
+ return true;
166
+ if (mode === "actionables")
167
+ return false;
168
+ return SEMANTIC_ROLES.has(normalized);
169
+ }
170
+ function formatNode(node) {
171
+ const name = redactText(node.name || "");
172
+ const value = redactText(node.value || "");
173
+ const parts = [];
174
+ parts.push(`[${node.ref}]`);
175
+ parts.push(node.role);
176
+ if (node.disabled) {
177
+ parts.push("disabled");
178
+ }
179
+ if (node.checked) {
180
+ parts.push("checked");
181
+ }
182
+ if (name) {
183
+ parts.push(`\"${name}\"`);
184
+ }
185
+ if (value) {
186
+ parts.push(`value=\"${value}\"`);
187
+ }
188
+ return parts.join(" ");
189
+ }
190
+ function redactText(text) {
191
+ const trimmed = (text ?? "").trim();
192
+ if (!trimmed)
193
+ return "";
194
+ return trimmed.replace(/[A-Za-z0-9+/_-]{24,}/g, "[redacted]");
195
+ }
196
+ function extractValue(value) {
197
+ if (!value || typeof value.value === "undefined" || value.value === null)
198
+ return "";
199
+ if (typeof value.value === "string")
200
+ return value.value;
201
+ if (typeof value.value === "number" || typeof value.value === "boolean") {
202
+ return String(value.value);
203
+ }
204
+ return "";
205
+ }
206
+ function isTruthyProperty(properties, name) {
207
+ if (!properties)
208
+ return false;
209
+ const found = properties.find((prop) => prop.name === name);
210
+ if (!found || !found.value)
211
+ return false;
212
+ const value = found.value.value;
213
+ if (typeof value === "boolean")
214
+ return value;
215
+ if (typeof value === "string")
216
+ return value.toLowerCase() === "true";
217
+ if (typeof value === "number")
218
+ return value !== 0;
219
+ return false;
220
+ }