@pxlarified/browser 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/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # OpenBrowser
2
+
3
+ OpenBrowser is a minimal browser-control skill and CLI. It controls one browser tab that it creates and owns, using a browser extension plus a local, user-scoped native bridge.
4
+
5
+ The npm package is intended to be published as `@pxlarified/browser` and used with:
6
+
7
+ ```bash
8
+ npx @pxlarified/browser <command>
9
+ ```
10
+
11
+ The OpenBrowser skill lives in `skills/OpenBrowser/` and is not included as an npm-published skill package.
12
+
13
+ ## Status
14
+
15
+ - Initial browser support: Zen, a Firefox-based browser.
16
+ - Extension artifact for Firefox-based browsers: `.xpi`.
17
+ - Chromium support is intentionally adapter-ready but not implemented yet.
18
+ - The default build creates an unsigned development `.xpi`.
19
+ - Firefox release signing is implemented as a separate `web-ext sign --channel unlisted` step.
20
+
21
+ ## Security model
22
+
23
+ The CLI does not expose browser control through a public network service. The extension talks to a native messaging host, and the CLI talks to that host through a user-scoped local bridge:
24
+
25
+ - Unix/macOS: `~/OpenBrowser/bridge/<browser>.sock` with user-only permissions.
26
+ - Windows: a user-local named pipe.
27
+
28
+ ## Session model
29
+
30
+ Each supported browser may have at most one active OpenBrowser session.
31
+
32
+ - A session is exactly one browser tab created by OpenBrowser.
33
+ - `open` fails if a session already exists for that browser.
34
+ - `close` closes only the OpenBrowser-owned tab.
35
+ - OpenBrowser never closes, reuses, inspects, navigates, or modifies tabs opened manually by the user.
36
+ - If the user manually closes the owned tab, the session is automatically considered closed.
37
+
38
+ ## Install and build
39
+
40
+ ```bash
41
+ npm install
42
+ npm run build
43
+ ```
44
+
45
+ This builds:
46
+
47
+ ```text
48
+ dist/extensions/firefox/openbrowser-dev.xpi
49
+ ```
50
+
51
+ The Firefox development artifact is unsigned. It is useful for local development and for browser builds/profiles that allow unsigned extensions.
52
+
53
+ ## Install the extension and native bridge
54
+
55
+ ```bash
56
+ npx @pxlarified/browser install zen
57
+ ```
58
+
59
+ The installer:
60
+
61
+ 1. Copies the bundled Firefox `.xpi` into detected Zen profiles as `openbrowser@mizius.com.xpi`.
62
+ 2. Installs the user-scoped native messaging manifest.
63
+ 3. Installs the native host launcher under `~/OpenBrowser/native-host/`.
64
+
65
+ If an OpenBrowser extension is already staged in a Zen profile, it is overwritten with the bundled version. Restart Zen if the update does not load immediately.
66
+
67
+ ## CLI commands
68
+
69
+ `--browser zen` is optional while Zen is the only supported browser.
70
+
71
+ ```bash
72
+ # Installation and session lifecycle
73
+ npx @pxlarified/browser install zen
74
+ npx @pxlarified/browser open <url> --browser zen
75
+ npx @pxlarified/browser close --browser zen
76
+ npx @pxlarified/browser status --browser zen
77
+
78
+ # Navigation
79
+ npx @pxlarified/browser navigate <url> --browser zen
80
+ npx @pxlarified/browser reload --browser zen
81
+ npx @pxlarified/browser back --browser zen
82
+ npx @pxlarified/browser forward --browser zen
83
+
84
+ # Page state
85
+ npx @pxlarified/browser state --browser zen
86
+
87
+ # Screenshots
88
+ npx @pxlarified/browser screenshot --browser zen
89
+ npx @pxlarified/browser screenshot --base64 --browser zen
90
+
91
+ # Interaction
92
+ npx @pxlarified/browser click <ref> --browser zen
93
+ npx @pxlarified/browser keys <text> --browser zen
94
+ npx @pxlarified/browser press <key> --browser zen
95
+ npx @pxlarified/browser select <ref> <option> --browser zen
96
+
97
+ # Content inspection
98
+ npx @pxlarified/browser get --html --browser zen
99
+ npx @pxlarified/browser get --html --ref <ref> --browser zen
100
+
101
+ # Scrolling
102
+ npx @pxlarified/browser scroll up [pixels] --browser zen
103
+ npx @pxlarified/browser scroll down [pixels] --browser zen
104
+ npx @pxlarified/browser scroll --to <ref> --browser zen
105
+ ```
106
+
107
+ Most commands print JSON to stdout. Screenshot commands are special:
108
+
109
+ - `screenshot` saves a PNG to `~/OpenBrowser/screenshots/<8-char-uuid>.png` and prints only the absolute file path to stdout.
110
+ - `screenshot --base64` prints only the Base64 PNG data to stdout.
111
+ - Diagnostics are written to stderr.
112
+
113
+ ## Page state and references
114
+
115
+ `state` returns the current URL, title, viewport, and actionable elements. Element references are generated by OpenBrowser and do not depend on raw webpage IDs.
116
+
117
+ ```json
118
+ {"url":"https://example.com","title":"Example Domain","elements":[{"ref":"e_1","role":"link","name":"More information"}]}
119
+ ```
120
+
121
+ References become invalid after navigation or relevant DOM updates. Commands that use invalid references return a clear `STALE_REFERENCE` error. Run `state` again to get fresh references.
122
+
123
+ ## Firefox signing and release pipeline
124
+
125
+ The development build is unsigned:
126
+
127
+ ```bash
128
+ npm run build:firefox
129
+ ```
130
+
131
+ When Mozilla Add-ons credentials are available, save them in a local uncommitted `.env` file:
132
+
133
+ ```env
134
+ AMO_JWT_ISSUER="..."
135
+ AMO_JWT_SECRET="..."
136
+ ```
137
+
138
+ Then create the signed unlisted release artifact with:
139
+
140
+ ```bash
141
+ npm run release:firefox
142
+ ```
143
+
144
+ You can also override the `.env` values with shell environment variables.
145
+
146
+ The release script runs the equivalent of:
147
+
148
+ ```bash
149
+ npx web-ext sign \
150
+ --source-dir ./extensions/ \
151
+ --channel unlisted \
152
+ --api-key "$AMO_JWT_ISSUER" \
153
+ --api-secret "$AMO_JWT_SECRET"
154
+ ```
155
+
156
+ It then bundles the resulting signed XPI at:
157
+
158
+ ```text
159
+ dist/extensions/firefox/openbrowser.xpi
160
+ ```
161
+
162
+ That signed `.xpi` is the artifact that should be included in the npm package for Firefox-based browser installs.
163
+
164
+ ### Signing information you need to provide
165
+
166
+ Before publishing a signed Firefox release, provide:
167
+
168
+ 1. `AMO_JWT_ISSUER` - the Mozilla Add-ons API key / JWT issuer.
169
+ 2. `AMO_JWT_SECRET` - the Mozilla Add-ons API secret / JWT secret.
170
+ 3. Confirmation of the final extension ID: `openbrowser@mizius.com`.
171
+ 4. Confirmation of the package/version to submit to AMO for unlisted signing.
172
+
173
+ ## Project layout
174
+
175
+ ```text
176
+ bin/OpenBrowser.js CLI entry point
177
+ src/ CLI, browser adapters, installer, native host
178
+ extensions/ Browser extension source
179
+ scripts/build.js Development XPI build
180
+ scripts/sign-firefox.js Unlisted Firefox signing pipeline
181
+ skills/OpenBrowser/SKILL.md Local agent skill, not npm-published
182
+ ```
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../src/cli.js";
3
+
4
+ runCli(process.argv.slice(2)).catch((error) => {
5
+ const message = error && error.message ? error.message : String(error);
6
+ console.error(message);
7
+ process.exitCode = 1;
8
+ });
@@ -0,0 +1,194 @@
1
+ const NATIVE_HOST = "openbrowser";
2
+ const SESSION_KEY = "session";
3
+ let port;
4
+ let reconnectTimer;
5
+
6
+ connectNativeHost();
7
+ browser.tabs.onRemoved.addListener(handleTabRemoved);
8
+
9
+ function connectNativeHost() {
10
+ try {
11
+ port = browser.runtime.connectNative(NATIVE_HOST);
12
+ port.onMessage.addListener(handleNativeMessage);
13
+ port.onDisconnect.addListener(() => {
14
+ port = null;
15
+ scheduleReconnect();
16
+ });
17
+ } catch (error) {
18
+ scheduleReconnect();
19
+ }
20
+ }
21
+
22
+ function scheduleReconnect() {
23
+ if (reconnectTimer) return;
24
+ reconnectTimer = setTimeout(() => {
25
+ reconnectTimer = null;
26
+ connectNativeHost();
27
+ }, 1000);
28
+ }
29
+
30
+ async function handleNativeMessage(message) {
31
+ const id = message && message.id;
32
+ if (!id) return;
33
+
34
+ try {
35
+ const result = await dispatchCommand(message.command, message.args || {});
36
+ postNative({ replyTo: id, ok: true, result });
37
+ } catch (error) {
38
+ postNative({
39
+ replyTo: id,
40
+ ok: false,
41
+ error: {
42
+ code: error.code || "COMMAND_FAILED",
43
+ message: error.message || String(error),
44
+ },
45
+ });
46
+ }
47
+ }
48
+
49
+ function postNative(message) {
50
+ if (!port) return;
51
+ port.postMessage(message);
52
+ }
53
+
54
+ async function dispatchCommand(command, args) {
55
+ switch (command) {
56
+ case "open": return openSession(args.url);
57
+ case "close": return closeSession();
58
+ case "status": return status();
59
+ case "navigate": return navigate(args.url);
60
+ case "reload": return tabAction((tabId) => browser.tabs.reload(tabId));
61
+ case "back": return contentCommand("historyBack", {});
62
+ case "forward": return contentCommand("historyForward", {});
63
+ case "state": return contentCommand("state", {});
64
+ case "screenshot": return screenshot();
65
+ case "click": return contentCommand("click", { ref: args.ref });
66
+ case "keys": return contentCommand("keys", { text: args.text });
67
+ case "press": return contentCommand("press", { key: args.key });
68
+ case "select": return contentCommand("select", { ref: args.ref, option: args.option });
69
+ case "getHtml": return contentCommand("getHtml", { ref: args.ref });
70
+ case "scroll": return contentCommand("scroll", args);
71
+ default: throw codedError("UNKNOWN_COMMAND", `Unknown OpenBrowser command: ${command}`);
72
+ }
73
+ }
74
+
75
+ async function openSession(url) {
76
+ const existing = await getLiveSession();
77
+ if (existing) throw codedError("SESSION_EXISTS", "An OpenBrowser session already exists for this browser. Close it first.");
78
+
79
+ const tab = await browser.tabs.create({ url });
80
+ const session = { tabId: tab.id, createdAt: new Date().toISOString() };
81
+ await browser.storage.local.set({ [SESSION_KEY]: session });
82
+ return { ok: true, session, url: tab.url || url };
83
+ }
84
+
85
+ async function closeSession() {
86
+ const session = await getStoredSession();
87
+ if (!session) return { ok: true, open: false };
88
+
89
+ try {
90
+ await browser.tabs.remove(session.tabId);
91
+ } catch {
92
+ // The owned tab may already have been manually closed.
93
+ }
94
+ await clearSession();
95
+ return { ok: true, open: false };
96
+ }
97
+
98
+ async function status() {
99
+ const session = await getLiveSession();
100
+ if (!session) return { open: false };
101
+ const tab = await browser.tabs.get(session.tabId);
102
+ return {
103
+ open: true,
104
+ session,
105
+ tab: {
106
+ id: tab.id,
107
+ url: tab.url,
108
+ title: tab.title,
109
+ status: tab.status,
110
+ },
111
+ };
112
+ }
113
+
114
+ async function navigate(url) {
115
+ const session = await requireLiveSession();
116
+ await browser.tabs.update(session.tabId, { url });
117
+ return { ok: true };
118
+ }
119
+
120
+ async function tabAction(action) {
121
+ const session = await requireLiveSession();
122
+ await action(session.tabId);
123
+ return { ok: true };
124
+ }
125
+
126
+ async function screenshot() {
127
+ const session = await requireLiveSession();
128
+ await browser.tabs.update(session.tabId, { active: true });
129
+ const dataUrl = await browser.tabs.captureTab(session.tabId, { format: "png" });
130
+ return { dataUrl };
131
+ }
132
+
133
+ async function contentCommand(type, payload) {
134
+ const session = await requireLiveSession();
135
+ await ensureContentScript(session.tabId);
136
+ try {
137
+ return await browser.tabs.sendMessage(session.tabId, { type, ...payload });
138
+ } catch (error) {
139
+ throw codedError("CONTENT_UNAVAILABLE", error.message || "OpenBrowser content script is unavailable on this page.");
140
+ }
141
+ }
142
+
143
+ async function ensureContentScript(tabId) {
144
+ try {
145
+ await browser.tabs.sendMessage(tabId, { type: "ping" });
146
+ return;
147
+ } catch {
148
+ // Inject below.
149
+ }
150
+
151
+ try {
152
+ await browser.tabs.executeScript(tabId, { file: "content.js", runAt: "document_idle" });
153
+ } catch (error) {
154
+ throw codedError("INJECTION_FAILED", `Cannot control this page: ${error.message || error}`);
155
+ }
156
+ }
157
+
158
+ async function getStoredSession() {
159
+ const stored = await browser.storage.local.get(SESSION_KEY);
160
+ return stored[SESSION_KEY] || null;
161
+ }
162
+
163
+ async function getLiveSession() {
164
+ const session = await getStoredSession();
165
+ if (!session) return null;
166
+ try {
167
+ await browser.tabs.get(session.tabId);
168
+ return session;
169
+ } catch {
170
+ await clearSession();
171
+ return null;
172
+ }
173
+ }
174
+
175
+ async function requireLiveSession() {
176
+ const session = await getLiveSession();
177
+ if (!session) throw codedError("NO_SESSION", "No active OpenBrowser session exists for this browser.");
178
+ return session;
179
+ }
180
+
181
+ async function clearSession() {
182
+ await browser.storage.local.remove(SESSION_KEY);
183
+ }
184
+
185
+ async function handleTabRemoved(tabId) {
186
+ const session = await getStoredSession();
187
+ if (session && session.tabId === tabId) await clearSession();
188
+ }
189
+
190
+ function codedError(code, message) {
191
+ const error = new Error(message);
192
+ error.code = code;
193
+ return error;
194
+ }
@@ -0,0 +1,275 @@
1
+ (function installOpenBrowserContentScript() {
2
+ if (window.__openbrowserContentInstalled) return;
3
+ window.__openbrowserContentInstalled = true;
4
+
5
+ var refs = new Map();
6
+ var generation = 0;
7
+
8
+ var observer = new MutationObserver(function () {
9
+ invalidateReferences();
10
+ });
11
+
12
+ observer.observe(document.documentElement || document, {
13
+ childList: true,
14
+ subtree: true,
15
+ attributes: true,
16
+ characterData: true,
17
+ attributeFilter: ["href", "disabled", "aria-label", "aria-hidden", "role", "tabindex", "value"],
18
+ });
19
+
20
+ window.addEventListener("pageshow", invalidateReferences, true);
21
+ window.addEventListener("hashchange", invalidateReferences, true);
22
+
23
+ browser.runtime.onMessage.addListener(function (message) {
24
+ if (!message || !message.type) return undefined;
25
+
26
+ try {
27
+ switch (message.type) {
28
+ case "ping": return Promise.resolve({ ok: true });
29
+ case "state": return Promise.resolve(getState());
30
+ case "click": return Promise.resolve(clickRef(message.ref));
31
+ case "keys": return Promise.resolve(typeKeys(message.text || ""));
32
+ case "press": return Promise.resolve(pressKey(message.key));
33
+ case "select": return Promise.resolve(selectOption(message.ref, message.option));
34
+ case "getHtml": return Promise.resolve(getHtml(message.ref));
35
+ case "scroll": return Promise.resolve(scrollPage(message));
36
+ case "historyBack": history.back(); return Promise.resolve({ ok: true });
37
+ case "historyForward": history.forward(); return Promise.resolve({ ok: true });
38
+ default: return Promise.reject(codedError("UNKNOWN_CONTENT_COMMAND", "Unknown content command."));
39
+ }
40
+ } catch (error) {
41
+ return Promise.reject(error);
42
+ }
43
+ });
44
+
45
+ function invalidateReferences() {
46
+ generation += 1;
47
+ refs.clear();
48
+ }
49
+
50
+ function getState() {
51
+ refs.clear();
52
+ var elements = collectActionableElements();
53
+ var stateGeneration = generation;
54
+ var result = [];
55
+
56
+ elements.forEach(function (element, index) {
57
+ var ref = "e_" + (index + 1);
58
+ refs.set(ref, { element: element, generation: stateGeneration });
59
+ result.push({
60
+ ref: ref,
61
+ role: roleOf(element),
62
+ name: accessibleName(element),
63
+ });
64
+ });
65
+
66
+ return {
67
+ url: location.href,
68
+ title: document.title,
69
+ viewport: {
70
+ width: window.innerWidth,
71
+ height: window.innerHeight,
72
+ scrollX: window.scrollX,
73
+ scrollY: window.scrollY,
74
+ devicePixelRatio: window.devicePixelRatio,
75
+ },
76
+ elements: result,
77
+ };
78
+ }
79
+
80
+ function collectActionableElements() {
81
+ var selector = [
82
+ "a[href]",
83
+ "button",
84
+ "input:not([type=hidden])",
85
+ "select",
86
+ "textarea",
87
+ "summary",
88
+ "[role=button]",
89
+ "[role=link]",
90
+ "[role=menuitem]",
91
+ "[role=checkbox]",
92
+ "[role=radio]",
93
+ "[tabindex]",
94
+ "[contenteditable=true]",
95
+ ].join(",");
96
+
97
+ return Array.prototype.slice.call(document.querySelectorAll(selector))
98
+ .filter(function (element) {
99
+ return isVisible(element) && !isDisabled(element) && accessibleName(element);
100
+ })
101
+ .slice(0, 200);
102
+ }
103
+
104
+ function getRef(ref) {
105
+ var entry = refs.get(ref);
106
+ if (!entry || entry.generation !== generation || !entry.element.isConnected) {
107
+ throw codedError("STALE_REFERENCE", "Stale OpenBrowser reference. Run state again and use a fresh ref.");
108
+ }
109
+ return entry.element;
110
+ }
111
+
112
+ function clickRef(ref) {
113
+ var element = getRef(ref);
114
+ element.scrollIntoView({ block: "center", inline: "center" });
115
+ element.focus({ preventScroll: true });
116
+ dispatchMouse(element, "mouseover");
117
+ dispatchMouse(element, "mousedown");
118
+ dispatchMouse(element, "mouseup");
119
+ if (typeof element.click === "function") element.click();
120
+ else dispatchMouse(element, "click");
121
+ return { ok: true };
122
+ }
123
+
124
+ function typeKeys(text) {
125
+ var element = document.activeElement;
126
+ if (!element || element === document.body) throw codedError("NO_FOCUS", "No focused element is available for text input.");
127
+
128
+ if (isTextInput(element)) {
129
+ insertText(element, text);
130
+ return { ok: true };
131
+ }
132
+
133
+ if (element.isContentEditable) {
134
+ document.execCommand("insertText", false, text);
135
+ element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: text }));
136
+ return { ok: true };
137
+ }
138
+
139
+ throw codedError("NOT_EDITABLE", "The focused element does not accept text input.");
140
+ }
141
+
142
+ function pressKey(key) {
143
+ var element = document.activeElement || document.body;
144
+ var options = { key: key, bubbles: true, cancelable: true };
145
+ element.dispatchEvent(new KeyboardEvent("keydown", options));
146
+ element.dispatchEvent(new KeyboardEvent("keypress", options));
147
+
148
+ if (key === "Enter" && typeof element.click === "function" && /^(BUTTON|A)$/.test(element.tagName)) element.click();
149
+ if (key === "Tab") focusNextElement();
150
+
151
+ element.dispatchEvent(new KeyboardEvent("keyup", options));
152
+ return { ok: true };
153
+ }
154
+
155
+ function selectOption(ref, option) {
156
+ var element = getRef(ref);
157
+ if (element.tagName !== "SELECT") throw codedError("NOT_SELECT", "Referenced element is not a select control.");
158
+
159
+ var options = Array.prototype.slice.call(element.options);
160
+ var match = options.find(function (candidate, index) {
161
+ return candidate.value === option || candidate.text.trim() === option || String(index) === option;
162
+ });
163
+ if (!match) throw codedError("OPTION_NOT_FOUND", "Select option was not found.");
164
+
165
+ element.value = match.value;
166
+ element.dispatchEvent(new Event("input", { bubbles: true }));
167
+ element.dispatchEvent(new Event("change", { bubbles: true }));
168
+ return { ok: true };
169
+ }
170
+
171
+ function getHtml(ref) {
172
+ if (!ref) return { html: document.documentElement.outerHTML };
173
+ return { html: getRef(ref).outerHTML };
174
+ }
175
+
176
+ function scrollPage(message) {
177
+ if (message.to) {
178
+ getRef(message.to).scrollIntoView({ block: "center", inline: "nearest" });
179
+ return { ok: true, scrollX: window.scrollX, scrollY: window.scrollY };
180
+ }
181
+
182
+ var pixels = Number(message.pixels || 600);
183
+ if (!Number.isFinite(pixels) || pixels <= 0) pixels = 600;
184
+ window.scrollBy({ top: message.direction === "up" ? -pixels : pixels, left: 0, behavior: "auto" });
185
+ return { ok: true, scrollX: window.scrollX, scrollY: window.scrollY };
186
+ }
187
+
188
+ function accessibleName(element) {
189
+ return (
190
+ element.getAttribute("aria-label") ||
191
+ element.getAttribute("title") ||
192
+ element.getAttribute("alt") ||
193
+ associatedLabel(element) ||
194
+ element.value ||
195
+ element.textContent ||
196
+ element.getAttribute("placeholder") ||
197
+ ""
198
+ ).replace(/\s+/g, " ").trim().slice(0, 160);
199
+ }
200
+
201
+ function associatedLabel(element) {
202
+ if (element.id) {
203
+ var label = document.querySelector('label[for="' + cssEscape(element.id) + '"]');
204
+ if (label) return label.textContent;
205
+ }
206
+ var parent = element.closest("label");
207
+ return parent ? parent.textContent : "";
208
+ }
209
+
210
+ function roleOf(element) {
211
+ var explicit = element.getAttribute("role");
212
+ if (explicit) return explicit;
213
+ var tag = element.tagName.toLowerCase();
214
+ if (tag === "a") return "link";
215
+ if (tag === "button") return "button";
216
+ if (tag === "select") return "select";
217
+ if (tag === "textarea") return "textbox";
218
+ if (tag === "summary") return "button";
219
+ if (tag === "input") {
220
+ var type = (element.getAttribute("type") || "text").toLowerCase();
221
+ if (["button", "submit", "reset"].includes(type)) return "button";
222
+ if (["checkbox", "radio"].includes(type)) return type;
223
+ return "textbox";
224
+ }
225
+ return "control";
226
+ }
227
+
228
+ function isVisible(element) {
229
+ var style = getComputedStyle(element);
230
+ if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity) === 0) return false;
231
+ var rect = element.getBoundingClientRect();
232
+ return rect.width > 0 && rect.height > 0;
233
+ }
234
+
235
+ function isDisabled(element) {
236
+ return Boolean(element.disabled || element.getAttribute("aria-disabled") === "true");
237
+ }
238
+
239
+ function dispatchMouse(element, type) {
240
+ element.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
241
+ }
242
+
243
+ function isTextInput(element) {
244
+ return element.tagName === "TEXTAREA" || (element.tagName === "INPUT" && !["button", "checkbox", "file", "radio", "reset", "submit"].includes((element.type || "text").toLowerCase()));
245
+ }
246
+
247
+ function insertText(element, text) {
248
+ var start = element.selectionStart || 0;
249
+ var end = element.selectionEnd || start;
250
+ var value = element.value || "";
251
+ element.value = value.slice(0, start) + text + value.slice(end);
252
+ var cursor = start + text.length;
253
+ element.setSelectionRange(cursor, cursor);
254
+ element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: text }));
255
+ element.dispatchEvent(new Event("change", { bubbles: true }));
256
+ }
257
+
258
+ function focusNextElement() {
259
+ var focusable = collectActionableElements();
260
+ var index = focusable.indexOf(document.activeElement);
261
+ var next = focusable[(index + 1) % focusable.length];
262
+ if (next) next.focus();
263
+ }
264
+
265
+ function cssEscape(value) {
266
+ if (window.CSS && CSS.escape) return CSS.escape(value);
267
+ return String(value).replace(/"/g, '\\"');
268
+ }
269
+
270
+ function codedError(code, message) {
271
+ var error = new Error(message);
272
+ error.code = code;
273
+ return error;
274
+ }
275
+ })();
@@ -0,0 +1,26 @@
1
+ {
2
+ "manifest_version": 2,
3
+ "name": "OpenBrowser",
4
+ "version": "0.1.0",
5
+ "description": "Local user-scoped browser control bridge for the OpenBrowser CLI.",
6
+ "permissions": [
7
+ "nativeMessaging",
8
+ "storage",
9
+ "tabs",
10
+ "activeTab",
11
+ "<all_urls>"
12
+ ],
13
+ "background": {
14
+ "scripts": ["background.js"],
15
+ "persistent": true
16
+ },
17
+ "browser_specific_settings": {
18
+ "gecko": {
19
+ "id": "openbrowser@mizius.com",
20
+ "strict_min_version": "109.0",
21
+ "data_collection_permissions": {
22
+ "required": ["none"]
23
+ }
24
+ }
25
+ }
26
+ }