Compare commits
4
Commits
1b32410575
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34aaf78c66
|
||
|
|
914508e2db
|
||
|
|
6352d9994e
|
||
|
|
541b950519
|
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
|
||||
import click
|
||||
from browser_cli.commands import client_from_ctx, handle_errors
|
||||
from browser_cli.commands import client_from_ctx, handle_errors, tab_option
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
@@ -12,10 +12,11 @@ def extract_group():
|
||||
"""Extract content from the active tab."""
|
||||
|
||||
@extract_group.command("links")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_links():
|
||||
"""Extract all links from the active tab."""
|
||||
links = client_from_ctx().extract.links()
|
||||
def extract_links(tab_id):
|
||||
"""Extract all links from the active tab or --tab."""
|
||||
links = client_from_ctx().extract.links(tab_id)
|
||||
if not links:
|
||||
console.print("[yellow]No links found[/yellow]")
|
||||
return
|
||||
@@ -27,10 +28,11 @@ def extract_links():
|
||||
console.print(table)
|
||||
|
||||
@extract_group.command("images")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_images():
|
||||
"""Extract all images from the active tab."""
|
||||
images = client_from_ctx().extract.images()
|
||||
def extract_images(tab_id):
|
||||
"""Extract all images from the active tab or --tab."""
|
||||
images = client_from_ctx().extract.images(tab_id)
|
||||
if not images:
|
||||
console.print("[yellow]No images found[/yellow]")
|
||||
return
|
||||
@@ -42,29 +44,33 @@ def extract_images():
|
||||
console.print(table)
|
||||
|
||||
@extract_group.command("text")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_text():
|
||||
"""Extract all visible text from the active tab."""
|
||||
console.print(client_from_ctx().extract.text())
|
||||
def extract_text(tab_id):
|
||||
"""Extract all visible text from the active tab or --tab."""
|
||||
console.print(client_from_ctx().extract.text(tab_id))
|
||||
|
||||
@extract_group.command("json")
|
||||
@click.argument("selector")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_json(selector):
|
||||
"""Parse and pretty-print JSON content inside SELECTOR."""
|
||||
data = client_from_ctx().extract.json(selector)
|
||||
def extract_json(selector, tab_id):
|
||||
"""Parse and pretty-print JSON content inside SELECTOR in the active tab or --tab."""
|
||||
data = client_from_ctx().extract.json(selector, tab_id)
|
||||
console.print_json(json.dumps(data))
|
||||
|
||||
@extract_group.command("html")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_html():
|
||||
"""Print the full HTML of the active tab to stdout."""
|
||||
click.echo(client_from_ctx().extract.html())
|
||||
def extract_html(tab_id):
|
||||
"""Print the full HTML of the active tab or --tab to stdout."""
|
||||
click.echo(client_from_ctx().extract.html(tab_id))
|
||||
|
||||
@extract_group.command("markdown")
|
||||
@click.option("--selector", help="Extract only the DOM subtree matching this CSS selector.")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_markdown(selector):
|
||||
"""Extract the page's main content as Markdown."""
|
||||
markdown = client_from_ctx().extract.markdown(selector)
|
||||
def extract_markdown(selector, tab_id):
|
||||
"""Extract the page's main content as Markdown from the active tab or --tab."""
|
||||
markdown = client_from_ctx().extract.markdown(selector, tab_id)
|
||||
click.echo(markdown or "", nl=not (markdown or "").endswith("\n"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import click
|
||||
from browser_cli.commands import client_from_ctx, handle_errors
|
||||
from browser_cli.commands import client_from_ctx, handle_errors, tab_option
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
@@ -10,10 +10,11 @@ def page_group():
|
||||
"""Inspect current page metadata."""
|
||||
|
||||
@page_group.command("info")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def page_info():
|
||||
"""Show title, URL, readyState, language, and meta tags of the active tab."""
|
||||
info = client_from_ctx().page.info()
|
||||
def page_info(tab_id):
|
||||
"""Show title, URL, readyState, language, and meta tags of the active tab or --tab."""
|
||||
info = client_from_ctx().page.info(tab_id)
|
||||
table = Table(show_header=False)
|
||||
table.add_column("Field", style="bold cyan", no_wrap=True)
|
||||
table.add_column("Value")
|
||||
|
||||
@@ -120,31 +120,37 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
|
||||
|
||||
@mcp.tool(name=tool_name("page_info", prefix))
|
||||
def page_info(
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return title, URL, readiness, language, and metadata for the active page."""
|
||||
return structured(_client(client_factory, browser, remote, key).page.info())
|
||||
"""Return title, URL, readiness, language, and metadata for the active page or tab_id."""
|
||||
client = _client(client_factory, browser, remote, key)
|
||||
if tab_id is not None:
|
||||
return structured(client.tabs.status(tab_id))
|
||||
return structured(client.page.info())
|
||||
|
||||
@mcp.tool(name=tool_name("extract_text", prefix))
|
||||
def extract_text(
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> str:
|
||||
"""Extract plain text from the active page."""
|
||||
return _client(client_factory, browser, remote, key).extract.text()
|
||||
"""Extract plain text from the active page or tab_id."""
|
||||
return _client(client_factory, browser, remote, key).extract.text(tab_id)
|
||||
|
||||
@mcp.tool(name=tool_name("extract_markdown", prefix))
|
||||
def extract_markdown(
|
||||
selector: str | None = None,
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> str:
|
||||
"""Extract clean Markdown from the active page or an optional CSS selector."""
|
||||
return _client(client_factory, browser, remote, key).extract.markdown(selector)
|
||||
"""Extract clean Markdown from the active page or tab_id, optionally scoped by CSS selector."""
|
||||
return _client(client_factory, browser, remote, key).extract.markdown(selector, tab_id)
|
||||
|
||||
@mcp.tool(name=tool_name("dom_query", prefix))
|
||||
def dom_query(
|
||||
|
||||
@@ -26,6 +26,7 @@ from browser_cli.framing import frame
|
||||
# hand back one the server has just timed out and closed.
|
||||
_MAX_IDLE_SECONDS = max(5, REMOTE_SESSION_IDLE_TIMEOUT - 5)
|
||||
_MAX_PER_ENDPOINT = 8
|
||||
_MAX_ENDPOINTS = 64
|
||||
|
||||
class PooledConnection:
|
||||
__slots__ = ("sock", "secret", "last_used")
|
||||
@@ -56,10 +57,32 @@ def checkout(endpoint: str) -> PooledConnection | None:
|
||||
_close(conn.sock) # too old — assume the server has dropped it
|
||||
return None
|
||||
|
||||
def _prune_endpoints_locked(now: float) -> None:
|
||||
"""Keep the number of endpoint buckets bounded for long-running SDK users."""
|
||||
for endpoint, bucket in list(_POOL.items()):
|
||||
fresh = [conn for conn in bucket if now - conn.last_used <= _MAX_IDLE_SECONDS]
|
||||
if fresh:
|
||||
_POOL[endpoint] = fresh
|
||||
else:
|
||||
for conn in bucket:
|
||||
_close(conn.sock)
|
||||
_POOL.pop(endpoint, None)
|
||||
|
||||
while len(_POOL) >= _MAX_ENDPOINTS:
|
||||
oldest_endpoint, bucket = min(
|
||||
_POOL.items(),
|
||||
key=lambda item: min(conn.last_used for conn in item[1]) if item[1] else 0.0,
|
||||
)
|
||||
for conn in bucket:
|
||||
_close(conn.sock)
|
||||
_POOL.pop(oldest_endpoint, None)
|
||||
|
||||
def checkin(endpoint: str, conn: PooledConnection) -> None:
|
||||
"""Return a still-healthy connection to the pool for reuse."""
|
||||
conn.last_used = time.monotonic()
|
||||
with _LOCK:
|
||||
if endpoint not in _POOL and len(_POOL) >= _MAX_ENDPOINTS:
|
||||
_prune_endpoints_locked(conn.last_used)
|
||||
bucket = _POOL.setdefault(endpoint, [])
|
||||
if len(bucket) >= _MAX_PER_ENDPOINT:
|
||||
_close(conn.sock)
|
||||
|
||||
+25
-23
@@ -131,39 +131,41 @@ class DomNS(Namespace):
|
||||
})
|
||||
|
||||
class ExtractNS(Namespace):
|
||||
"""Extract structured content from the active tab."""
|
||||
"""Extract structured content from the active (or specified) tab."""
|
||||
|
||||
@sdk_command("extract.links", default=[])
|
||||
def links(self) -> list[dict]:
|
||||
"""Return links from the active tab."""
|
||||
@sdk_command("extract.links", lambda self, tab_id=None: {"tabId": tab_id}, default=[])
|
||||
def links(self, tab_id: int | None = None) -> list[dict]:
|
||||
"""Return links from the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.images", default=[])
|
||||
def images(self) -> list[dict]:
|
||||
"""Return images from the active tab."""
|
||||
@sdk_command("extract.images", lambda self, tab_id=None: {"tabId": tab_id}, default=[])
|
||||
def images(self, tab_id: int | None = None) -> list[dict]:
|
||||
"""Return images from the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.text", default="")
|
||||
def text(self) -> str:
|
||||
"""Return plain text from the active tab."""
|
||||
@sdk_command("extract.text", lambda self, tab_id=None: {"tabId": tab_id}, default="")
|
||||
def text(self, tab_id: int | None = None) -> str:
|
||||
"""Return plain text from the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.json", lambda self, selector: {"selector": selector})
|
||||
def json(self, selector: str):
|
||||
"""Extract JSON-like structured data from a selector."""
|
||||
@sdk_command("extract.json", lambda self, selector, tab_id=None: {"selector": selector, "tabId": tab_id})
|
||||
def json(self, selector: str, tab_id: int | None = None):
|
||||
"""Extract JSON-like structured data from a selector in the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.html", default="")
|
||||
def html(self) -> str:
|
||||
"""Return the full HTML source of the active tab."""
|
||||
@sdk_command("extract.html", lambda self, tab_id=None: {"tabId": tab_id}, default="")
|
||||
def html(self, tab_id: int | None = None) -> str:
|
||||
"""Return the full HTML source of the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.markdown", lambda self, selector=None: {"selector": selector}, mapper=_extract_markdown)
|
||||
def markdown(self, selector: str | None = None) -> str:
|
||||
"""Extract the page's main content as clean Markdown.
|
||||
@sdk_command("extract.markdown", lambda self, selector=None, tab_id=None: {"selector": selector, "tabId": tab_id}, mapper=_extract_markdown)
|
||||
def markdown(self, selector: str | None = None, tab_id: int | None = None) -> str:
|
||||
"""Extract the page's main content as clean Markdown from the active tab or *tab_id*.
|
||||
|
||||
The extractor may return either Markdown or raw HTML; both are
|
||||
normalized to Markdown here so SDK and CLI callers get identical output.
|
||||
"""
|
||||
|
||||
class PageNS(Namespace):
|
||||
"""Inspect the active page."""
|
||||
"""Inspect the active page or a specified tab."""
|
||||
|
||||
@sdk_command("page.info", default={})
|
||||
def info(self) -> dict:
|
||||
"""Return title, URL, readyState, lang, and meta tags of the active tab."""
|
||||
def info(self, tab_id: int | None = None) -> dict:
|
||||
"""Return metadata for the active page, or tab status for *tab_id*."""
|
||||
if tab_id is not None:
|
||||
return self.command("tabs.status", {"tabId": tab_id}) or {}
|
||||
return self.command("page.info", {}) or {}
|
||||
|
||||
@@ -70,19 +70,48 @@ class RateLimiter:
|
||||
``rate`` is the sustained refill in tokens/second; ``burst`` is the bucket
|
||||
capacity (defaults to ``rate``). ``rate <= 0`` disables limiting entirely.
|
||||
Thread-safe so it can be shared across all connections of one serve process.
|
||||
|
||||
The bucket table is capped. Without that bound, a long-running public server
|
||||
could retain one entry per ever-seen identity/IP forever; GC cannot reclaim
|
||||
those entries because the limiter still references them.
|
||||
"""
|
||||
|
||||
def __init__(self, rate: float, burst: float | None = None) -> None:
|
||||
def __init__(self, rate: float, burst: float | None = None, max_buckets: int = 4096) -> None:
|
||||
self.rate = float(rate)
|
||||
self.capacity = float(burst) if burst is not None else max(float(rate), 1.0)
|
||||
self.max_buckets = max(1, int(max_buckets))
|
||||
self._buckets: dict[str, tuple[float, float]] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _prune_locked(self, now: float) -> None:
|
||||
"""Drop idle/full buckets, then oldest buckets, until the table is bounded."""
|
||||
if len(self._buckets) < self.max_buckets or self.rate <= 0:
|
||||
return
|
||||
|
||||
# Once a bucket has fully refilled, keeping it around carries no useful
|
||||
# throttling state. Use at least 60s so normal active identities are not
|
||||
# churned out aggressively on high-rate configs.
|
||||
idle_seconds = max(60.0, (self.capacity / self.rate) * 2)
|
||||
full_epsilon = 1e-9
|
||||
for bucket_key, (tokens, last) in list(self._buckets.items()):
|
||||
refilled = min(self.capacity, tokens + (now - last) * self.rate)
|
||||
if refilled >= self.capacity - full_epsilon and now - last >= idle_seconds:
|
||||
self._buckets.pop(bucket_key, None)
|
||||
|
||||
# If an attacker keeps creating fresh identities faster than they go idle,
|
||||
# still keep memory bounded. Evict the oldest identity state; that may reset
|
||||
# throttling for that identity, but bounded memory is more important here.
|
||||
while len(self._buckets) >= self.max_buckets:
|
||||
oldest_key = min(self._buckets, key=lambda k: self._buckets[k][1])
|
||||
self._buckets.pop(oldest_key, None)
|
||||
|
||||
def allow(self, key: str) -> bool:
|
||||
if self.rate <= 0:
|
||||
return True
|
||||
now = time.monotonic()
|
||||
with self._lock:
|
||||
if key not in self._buckets and len(self._buckets) >= self.max_buckets:
|
||||
self._prune_locked(now)
|
||||
tokens, last = self._buckets.get(key, (self.capacity, now))
|
||||
tokens = min(self.capacity, tokens + (now - last) * self.rate)
|
||||
if tokens < 1.0:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "browser-cli",
|
||||
"version": "0.16.6",
|
||||
"version": "0.16.7",
|
||||
"description": "Control your browser from the terminal or Python SDK",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
|
||||
@@ -15,6 +15,11 @@ import type { Job, Serializable, ErrorLike, DispatchArgs } from '../types';
|
||||
// jobs only need to survive long enough for the CLI to poll their result.
|
||||
export const MAX_FINISHED_JOBS = 20;
|
||||
|
||||
// Cap simultaneously running background jobs. A hung job has a watchdog, but a
|
||||
// command flood could still pin many timers/results for up to JOB_TIMEOUT_MS.
|
||||
// Rejecting above this bound keeps service-worker memory predictable.
|
||||
export const MAX_RUNNING_JOBS = 32;
|
||||
|
||||
// Watchdog: if a runner never resolves/rejects (e.g. executeScript against a
|
||||
// dead tab), finalize the job as an error so its persist interval stops instead
|
||||
// of writing to api.storage.local every second forever.
|
||||
@@ -77,6 +82,11 @@ export class JobManager {
|
||||
}
|
||||
|
||||
async start(command: string, args: DispatchArgs, runner: JobRunner) {
|
||||
const runningCount = [...this.jobs.values()].filter(job => job.status === "running").length;
|
||||
if (runningCount >= MAX_RUNNING_JOBS) {
|
||||
throw new Error(`too many background jobs running (${runningCount}); wait for jobs to finish or cancel one`);
|
||||
}
|
||||
|
||||
const jobId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const job: Job = {
|
||||
id: jobId,
|
||||
|
||||
@@ -56,7 +56,7 @@ export class DomCommands extends CommandGroup {
|
||||
"dom.poll": (a: DomPollArgs) => this.domPoll(a),
|
||||
|
||||
// ── Page ─────────────────────────────────────────────────────────────
|
||||
"page.info": () => this.domOp("pageInfo", {}),
|
||||
"page.info": (a: DomArgs) => this.domOp("pageInfo", a),
|
||||
|
||||
// ── Extract ──────────────────────────────────────────────────────────
|
||||
"extract.links": (a: DomArgs) => this.domOp("extractLinks", a),
|
||||
@@ -64,7 +64,7 @@ export class DomCommands extends CommandGroup {
|
||||
"extract.text": (a: DomArgs) => this.domOp("extractText", a),
|
||||
"extract.json": (a: DomArgs) => this.domOp("extractJson", a),
|
||||
"extract.markdown": (a: DomArgs) => this.domOp("extractMarkdown", a),
|
||||
"extract.html": () => fetchTabHtml(undefined),
|
||||
"extract.html": (a: DomArgs = {}) => fetchTabHtml(a.tabId),
|
||||
};
|
||||
|
||||
private async domOp(funcName: string, args: DomArgs = {}) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getLargeOperationThrottle, getPerformanceProfile, hasAudibleTabs, setPerformanceProfile } from '../core';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import type { CommandEntry } from '../classes/CommandGroup';
|
||||
import type { PerfSetProfileArgs, JobIdArgs } from '../types';
|
||||
import type { Job, PerfSetProfileArgs, JobIdArgs } from '../types';
|
||||
|
||||
// PerfCommands also owns the jobs.* status/cancel queries: they read the same
|
||||
// JobManager (ctx.jobs) that perf.status reports, and there is no dedicated
|
||||
@@ -15,15 +15,8 @@ export class PerfCommands extends CommandGroup {
|
||||
"jobs.cancel": (a: JobIdArgs) => this.ctx.jobs.cancel(a),
|
||||
};
|
||||
|
||||
private async perfStatus() {
|
||||
const profile = await getPerformanceProfile();
|
||||
const audible = await hasAudibleTabs();
|
||||
const throttle = await getLargeOperationThrottle(0, "auto");
|
||||
private jobSummary(job: Job) {
|
||||
return {
|
||||
performanceProfile: profile,
|
||||
audible,
|
||||
throttle,
|
||||
jobs: this.ctx.jobs.list().map(job => ({
|
||||
id: job.id,
|
||||
command: job.command,
|
||||
status: job.status,
|
||||
@@ -32,7 +25,18 @@ export class PerfCommands extends CommandGroup {
|
||||
total: job.total,
|
||||
percent: job.percent,
|
||||
cancelRequested: job.cancelRequested,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private async perfStatus() {
|
||||
const profile = await getPerformanceProfile();
|
||||
const audible = await hasAudibleTabs();
|
||||
const throttle = await getLargeOperationThrottle(0, "auto");
|
||||
return {
|
||||
performanceProfile: profile,
|
||||
audible,
|
||||
throttle,
|
||||
jobs: this.ctx.jobs.list().map(job => this.jobSummary(job)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,31 @@ export class WindowsCommands extends CommandGroup {
|
||||
"windows.open": (a: WindowsOpenArgs) => this.windowsOpen(a),
|
||||
};
|
||||
|
||||
private async activeWindowIds(): Promise<Set<number>> {
|
||||
const windows = await api.windows.getAll({});
|
||||
return new Set(windows.map(w => w.id).filter(id => typeof id === "number"));
|
||||
}
|
||||
|
||||
private async pruneAliases(activeIds?: Set<number>): Promise<Record<string, string>> {
|
||||
const aliases = await getAliases();
|
||||
const liveIds = activeIds || await this.activeWindowIds();
|
||||
const pruned: Record<string, string> = {};
|
||||
let changed = false;
|
||||
for (const [id, alias] of Object.entries(aliases)) {
|
||||
if (liveIds.has(Number(id))) {
|
||||
pruned[id] = alias;
|
||||
} else {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) await api.storage.local.set({ windowAliases: pruned });
|
||||
return pruned;
|
||||
}
|
||||
|
||||
private async windowsList() {
|
||||
const windows = await api.windows.getAll({ populate: true });
|
||||
const aliases = await getAliases();
|
||||
const activeIds = new Set(windows.map(w => w.id).filter(id => typeof id === "number"));
|
||||
const aliases = await this.pruneAliases(activeIds);
|
||||
return windows.map(w => ({
|
||||
id: w.id,
|
||||
alias: aliases[w.id] || null,
|
||||
@@ -27,7 +49,7 @@ export class WindowsCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async windowsRename({ windowId, name }: WindowsRenameArgs) {
|
||||
const aliases = await getAliases();
|
||||
const aliases = await this.pruneAliases();
|
||||
aliases[windowId] = name;
|
||||
await api.storage.local.set({ windowAliases: aliases });
|
||||
return { windowId, name };
|
||||
@@ -35,6 +57,11 @@ export class WindowsCommands extends CommandGroup {
|
||||
|
||||
private async windowsClose({ windowId }: WindowsCloseArgs) {
|
||||
await api.windows.remove(windowId);
|
||||
const aliases = await this.pruneAliases();
|
||||
if (windowId in aliases) {
|
||||
delete aliases[windowId];
|
||||
await api.storage.local.set({ windowAliases: aliases });
|
||||
}
|
||||
return { windowId };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { test, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { JobManager, JOB_TIMEOUT_MS, MAX_FINISHED_JOBS, pruneFinishedJobs } from '../src/classes/JobManager';
|
||||
import { JobManager, JOB_TIMEOUT_MS, MAX_FINISHED_JOBS, MAX_RUNNING_JOBS, pruneFinishedJobs } from '../src/classes/JobManager';
|
||||
import { makeChromeMock } from './chrome-mock';
|
||||
|
||||
// Drain pending microtasks (finalize() chains several awaits). setImmediate is
|
||||
@@ -129,6 +129,21 @@ test('JobManager: a runner that settles after the watchdog cannot resurrect the
|
||||
mock.timers.reset();
|
||||
});
|
||||
|
||||
test('JobManager: rejects new background jobs above the running-job cap', async () => {
|
||||
mock.timers.enable({ apis: ['setInterval', 'setTimeout'] });
|
||||
globalThis.chrome = makeChromeMock();
|
||||
const mgr = new JobManager();
|
||||
for (let i = 0; i < MAX_RUNNING_JOBS; i++) {
|
||||
await mgr.start(`running${i}`, {}, () => new Promise(() => {}));
|
||||
}
|
||||
await assert.rejects(
|
||||
() => mgr.start('overflow', {}, async () => 'nope'),
|
||||
/too many background jobs running/,
|
||||
);
|
||||
assert.equal(mgr.list().filter(job => job.status === 'running').length, MAX_RUNNING_JOBS);
|
||||
mock.timers.reset();
|
||||
});
|
||||
|
||||
test('JobManager: persisted set keeps running jobs even past the finished cap', async () => {
|
||||
mock.timers.enable({ apis: ['setInterval', 'setTimeout'] });
|
||||
globalThis.chrome = makeChromeMock();
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// @ts-nocheck
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { WindowsCommands } from '../src/commands/windows';
|
||||
import { makeChromeMock } from './chrome-mock';
|
||||
|
||||
function makeWindowsChromeMock(windows) {
|
||||
const chrome = makeChromeMock();
|
||||
chrome.windows = {
|
||||
getAll: async () => windows,
|
||||
remove: async () => {},
|
||||
create: async () => ({ id: 99 }),
|
||||
};
|
||||
return chrome;
|
||||
}
|
||||
|
||||
test('windows.list prunes aliases for closed windows', async () => {
|
||||
globalThis.chrome = makeWindowsChromeMock([
|
||||
{ id: 1, focused: true, state: 'normal', tabs: [{ id: 10 }] },
|
||||
{ id: 2, focused: false, state: 'minimized', tabs: [] },
|
||||
]);
|
||||
globalThis.chrome.storage.local._store.windowAliases = {
|
||||
1: 'main',
|
||||
2: 'side',
|
||||
999: 'closed',
|
||||
};
|
||||
|
||||
const commands = new WindowsCommands({ jobs: {} });
|
||||
const result = await commands.commands['windows.list']({});
|
||||
|
||||
assert.deepEqual(result.map(w => [w.id, w.alias]), [[1, 'main'], [2, 'side']]);
|
||||
assert.deepEqual(globalThis.chrome.storage.local._store.windowAliases, { 1: 'main', 2: 'side' });
|
||||
});
|
||||
|
||||
test('windows.rename prunes stale aliases before saving the new name', async () => {
|
||||
globalThis.chrome = makeWindowsChromeMock([
|
||||
{ id: 1, focused: true, state: 'normal', tabs: [] },
|
||||
{ id: 2, focused: false, state: 'normal', tabs: [] },
|
||||
]);
|
||||
globalThis.chrome.storage.local._store.windowAliases = {
|
||||
1: 'main',
|
||||
999: 'closed',
|
||||
};
|
||||
|
||||
const commands = new WindowsCommands({ jobs: {} });
|
||||
await commands.commands['windows.rename']({ windowId: 2, name: 'work' });
|
||||
|
||||
assert.deepEqual(globalThis.chrome.storage.local._store.windowAliases, { 1: 'main', 2: 'work' });
|
||||
});
|
||||
|
||||
test('windows.close removes the closed window alias immediately', async () => {
|
||||
let removed = null;
|
||||
globalThis.chrome = makeWindowsChromeMock([
|
||||
{ id: 1, focused: true, state: 'normal', tabs: [] },
|
||||
{ id: 2, focused: false, state: 'normal', tabs: [] },
|
||||
]);
|
||||
globalThis.chrome.windows.remove = async id => { removed = id; };
|
||||
globalThis.chrome.storage.local._store.windowAliases = {
|
||||
1: 'main',
|
||||
2: 'side',
|
||||
999: 'closed',
|
||||
};
|
||||
|
||||
const commands = new WindowsCommands({ jobs: {} });
|
||||
await commands.commands['windows.close']({ windowId: 2 });
|
||||
|
||||
assert.equal(removed, 2);
|
||||
assert.deepEqual(globalThis.chrome.storage.local._store.windowAliases, { 1: 'main' });
|
||||
});
|
||||
Generated
+1309
-216
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-nodes-browser-cli",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"description": "n8n community node that controls a remote browser by talking directly to a browser-cli serve endpoint (Ed25519 + post-quantum encrypted)",
|
||||
"keywords": [
|
||||
"n8n-community-node-package",
|
||||
@@ -31,15 +31,15 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"n8n-workflow": "*",
|
||||
"typescript": "^5.6.0"
|
||||
"@types/node": "^26.4.1",
|
||||
"esbuild": "^0.28.2",
|
||||
"n8n-workflow": "2.37.4",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"n8n-workflow": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/post-quantum": "^0.6.1"
|
||||
"@noble/post-quantum": "^0.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2021"],
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
|
||||
Generated
+480
-119
@@ -6,16 +6,16 @@
|
||||
"": {
|
||||
"name": "browser-cli-extension-build",
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.1.40",
|
||||
"@types/chrome": "^0.2.9",
|
||||
"@types/firefox-webext-browser": "^143.0.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"typescript": "^6.0.3"
|
||||
"esbuild": "^0.28.2",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -30,9 +30,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -47,9 +47,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -64,9 +64,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -81,9 +81,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -98,9 +98,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -115,9 +115,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -132,9 +132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -149,9 +149,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -166,9 +166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -183,9 +183,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -200,9 +200,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -217,9 +217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -234,9 +234,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -251,9 +251,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -268,9 +268,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -285,9 +285,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -302,9 +302,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -319,9 +319,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -336,9 +336,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -353,9 +353,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -370,9 +370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -387,9 +387,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -404,9 +404,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -421,9 +421,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -438,9 +438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -455,9 +455,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chrome": {
|
||||
"version": "0.1.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.43.tgz",
|
||||
"integrity": "sha512-ukH/HhmR6ht+UTX3PLUWJxgJ/RQcK2Foj4lBzsF24SIWsXgqhGuXqjd8FFuwioPP7d/JUKLM4g8GZxw3F4HTcA==",
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.2.9.tgz",
|
||||
"integrity": "sha512-6rXrDPkkWv4D2Q23HqT+iED4voODU5CpOSc2VFgKyecSFItuEsIBvz9CY6jUQ3dd+Q7zo1bnCE9pKRMtkYut5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -496,10 +496,350 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript/typescript-aix-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
|
||||
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-loong64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
|
||||
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-mips64el": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
|
||||
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-riscv64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
|
||||
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-s390x": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
|
||||
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-sunos-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
||||
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -510,46 +850,67 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
"@esbuild/aix-ppc64": "0.28.2",
|
||||
"@esbuild/android-arm": "0.28.2",
|
||||
"@esbuild/android-arm64": "0.28.2",
|
||||
"@esbuild/android-x64": "0.28.2",
|
||||
"@esbuild/darwin-arm64": "0.28.2",
|
||||
"@esbuild/darwin-x64": "0.28.2",
|
||||
"@esbuild/freebsd-arm64": "0.28.2",
|
||||
"@esbuild/freebsd-x64": "0.28.2",
|
||||
"@esbuild/linux-arm": "0.28.2",
|
||||
"@esbuild/linux-arm64": "0.28.2",
|
||||
"@esbuild/linux-ia32": "0.28.2",
|
||||
"@esbuild/linux-loong64": "0.28.2",
|
||||
"@esbuild/linux-mips64el": "0.28.2",
|
||||
"@esbuild/linux-ppc64": "0.28.2",
|
||||
"@esbuild/linux-riscv64": "0.28.2",
|
||||
"@esbuild/linux-s390x": "0.28.2",
|
||||
"@esbuild/linux-x64": "0.28.2",
|
||||
"@esbuild/netbsd-arm64": "0.28.2",
|
||||
"@esbuild/netbsd-x64": "0.28.2",
|
||||
"@esbuild/openbsd-arm64": "0.28.2",
|
||||
"@esbuild/openbsd-x64": "0.28.2",
|
||||
"@esbuild/openharmony-arm64": "0.28.2",
|
||||
"@esbuild/sunos-x64": "0.28.2",
|
||||
"@esbuild/win32-arm64": "0.28.2",
|
||||
"@esbuild/win32-ia32": "0.28.2",
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
||||
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
"tsc": "bin/tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=16.20.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@typescript/typescript-aix-ppc64": "7.0.2",
|
||||
"@typescript/typescript-darwin-arm64": "7.0.2",
|
||||
"@typescript/typescript-darwin-x64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-x64": "7.0.2",
|
||||
"@typescript/typescript-linux-arm": "7.0.2",
|
||||
"@typescript/typescript-linux-arm64": "7.0.2",
|
||||
"@typescript/typescript-linux-loong64": "7.0.2",
|
||||
"@typescript/typescript-linux-mips64el": "7.0.2",
|
||||
"@typescript/typescript-linux-ppc64": "7.0.2",
|
||||
"@typescript/typescript-linux-riscv64": "7.0.2",
|
||||
"@typescript/typescript-linux-s390x": "7.0.2",
|
||||
"@typescript/typescript-linux-x64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-sunos-x64": "7.0.2",
|
||||
"@typescript/typescript-win32-arm64": "7.0.2",
|
||||
"@typescript/typescript-win32-x64": "7.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -13,9 +13,9 @@
|
||||
"package:extension:firefox": "npm run build:extension && python scripts/package_extension.py --firefox"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.1.40",
|
||||
"@types/chrome": "^0.2.9",
|
||||
"@types/firefox-webext-browser": "^143.0.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"typescript": "^6.0.3"
|
||||
"esbuild": "^0.28.2",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "real-browser-cli"
|
||||
version = "0.16.6"
|
||||
version = "0.16.7"
|
||||
description = "Control your real running browser from the terminal or Python SDK"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
+14
-3
@@ -299,22 +299,28 @@ class TestExtract:
|
||||
result = b.extract.markdown()
|
||||
|
||||
assert result == "# Title"
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None, "tabId": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_markdown_selector(self, b, mock_send):
|
||||
mock_send.return_value = "## Post"
|
||||
|
||||
assert b.extract.markdown("article") == "## Post"
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": "article", "tabId": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_links(self, b, mock_send):
|
||||
mock_send.return_value = [{"href": "https://x"}]
|
||||
assert b.extract.links() == [{"href": "https://x"}]
|
||||
mock_send.assert_called_once_with("extract.links", {}, profile=None, remote=None, key=None)
|
||||
mock_send.assert_called_once_with("extract.links", {"tabId": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_text_none(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
assert b.extract.text() == ""
|
||||
mock_send.assert_called_once_with("extract.text", {"tabId": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_markdown_tab_id(self, b, mock_send):
|
||||
mock_send.return_value = "# Specific"
|
||||
assert b.extract.markdown(tab_id=42) == "# Specific"
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None, "tabId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
# ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -869,6 +875,11 @@ class TestPageStorageCookies:
|
||||
assert b.page.info() == {"title": "X"}
|
||||
mock_send.assert_called_once_with("page.info", {}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_page_info_tab_id(self, b, mock_send):
|
||||
mock_send.return_value = {"title": "X"}
|
||||
assert b.page.info(tab_id=42) == {"title": "X"}
|
||||
mock_send.assert_called_once_with("tabs.status", {"tabId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_storage_get(self, b, mock_send):
|
||||
mock_send.return_value = "v"
|
||||
assert b.storage.get("k") == "v"
|
||||
|
||||
+18
-2
@@ -796,13 +796,29 @@ def test_windows_open_passes_url():
|
||||
assert "https://example.com" in result.output
|
||||
send_command.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_page_info_command_with_tab():
|
||||
with patch("browser_cli.send_command", return_value={"title": "Example", "url": "https://example.com"}) as send_command:
|
||||
result = CliRunner().invoke(main, ["page", "info", "--tab", "42"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Example" in result.output
|
||||
send_command.assert_called_once_with("tabs.status", {"tabId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_markdown_command():
|
||||
with patch("browser_cli.send_command", return_value="# Title") as send_command:
|
||||
result = CliRunner().invoke(main, ["extract", "markdown"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.output == "# Title\n"
|
||||
send_command.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
|
||||
send_command.assert_called_once_with("extract.markdown", {"selector": None, "tabId": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_markdown_command_with_tab():
|
||||
with patch("browser_cli.send_command", return_value="# Title") as send_command:
|
||||
result = CliRunner().invoke(main, ["extract", "markdown", "--tab", "42"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.output == "# Title\n"
|
||||
send_command.assert_called_once_with("extract.markdown", {"selector": None, "tabId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_markdown_command_with_selector():
|
||||
with patch("browser_cli.send_command", return_value="## Post") as send_command:
|
||||
@@ -810,7 +826,7 @@ def test_extract_markdown_command_with_selector():
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.output == "## Post\n"
|
||||
send_command.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
|
||||
send_command.assert_called_once_with("extract.markdown", {"selector": "article", "tabId": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_clean_markdown_output_removes_escaped_underscores_and_dashes():
|
||||
assert _clean_markdown_output(r"hello\_world \- item") == "hello_world - item"
|
||||
|
||||
+26
-5
@@ -62,14 +62,17 @@ class FakeClient:
|
||||
def navigate_to(self, tab_id, url):
|
||||
self.calls.append(("navigate", tab_id, url))
|
||||
|
||||
def page_info(self):
|
||||
return {"title": "Example", "url": "https://example.com"}
|
||||
def page_info(self, tab_id=None):
|
||||
self.calls.append(("page_info", tab_id))
|
||||
return {"title": "Example", "url": "https://example.com", "tab_id": tab_id}
|
||||
|
||||
def extract_text(self):
|
||||
def extract_text(self, tab_id=None):
|
||||
self.calls.append(("extract_text", tab_id))
|
||||
return "Page text"
|
||||
|
||||
def extract_markdown(self, selector=None):
|
||||
return f"# Page {selector or ''}".rstrip()
|
||||
def extract_markdown(self, selector=None, tab_id=None):
|
||||
self.calls.append(("extract_markdown", selector, tab_id))
|
||||
return f"# Page {selector or ''} {tab_id or ''}".rstrip()
|
||||
|
||||
def dom_query(self, selector):
|
||||
return [{"tag": "button", "selector": selector}]
|
||||
@@ -112,6 +115,7 @@ async def test_each_tool_call_constructs_a_fresh_targeted_client(client, monkeyp
|
||||
assert second.structured_content == {
|
||||
"title": "Example",
|
||||
"url": "https://example.com",
|
||||
"tab_id": None,
|
||||
}
|
||||
assert len(FakeClient.instances) == 2
|
||||
assert FakeClient.instances[0].target == ("work", "browser-host.example:443", "agent")
|
||||
@@ -195,6 +199,23 @@ async def test_tab_tools_default_to_the_active_tab(client):
|
||||
]
|
||||
assert FakeClient.instances[1].calls == [("active",), ("close", 7)]
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_read_tools_accept_explicit_tab_id(client):
|
||||
info = await client.call_tool("browser_page_info", {"tab_id": 42})
|
||||
text = await client.call_tool("browser_extract_text", {"tab_id": 42})
|
||||
markdown = await client.call_tool("browser_extract_markdown", {"selector": "main", "tab_id": 42})
|
||||
|
||||
assert info.structured_content == {
|
||||
"id": 42,
|
||||
"title": "Navigated",
|
||||
"url": "https://example.com/next",
|
||||
}
|
||||
assert text.content[0].text == "Page text"
|
||||
assert markdown.content[0].text == "# Page main 42"
|
||||
assert FakeClient.instances[0].calls == [("status", 42)]
|
||||
assert FakeClient.instances[1].calls == [("extract_text", 42)]
|
||||
assert FakeClient.instances[2].calls == [("extract_markdown", "main", 42)]
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_screenshot_returns_image_content(client):
|
||||
result = await client.call_tool("browser_screenshot", {"tab_id": 7, "format": "png"})
|
||||
|
||||
@@ -43,6 +43,39 @@ def test_checkin_caps_pool_size():
|
||||
b.close()
|
||||
pool.close_all()
|
||||
|
||||
def test_checkin_caps_endpoint_buckets():
|
||||
pool.close_all()
|
||||
peers = []
|
||||
try:
|
||||
for i in range(pool._MAX_ENDPOINTS + 5):
|
||||
a, b = _socketpair()
|
||||
peers.append(b)
|
||||
pool.checkin(f"host-{i}:443", pool.PooledConnection(a, b"secret"))
|
||||
assert len(pool._POOL) <= pool._MAX_ENDPOINTS
|
||||
finally:
|
||||
for peer in peers:
|
||||
peer.close()
|
||||
pool.close_all()
|
||||
|
||||
def test_checkin_prunes_stale_endpoint_buckets():
|
||||
pool.close_all()
|
||||
old_a, old_b = _socketpair()
|
||||
old = pool.PooledConnection(old_a, b"secret")
|
||||
pool.checkin("old:443", old)
|
||||
old.last_used -= pool._MAX_IDLE_SECONDS + 1
|
||||
peers = [old_b]
|
||||
try:
|
||||
for i in range(pool._MAX_ENDPOINTS):
|
||||
a, b = _socketpair()
|
||||
peers.append(b)
|
||||
pool.checkin(f"new-{i}:443", pool.PooledConnection(a, b"secret"))
|
||||
assert "old:443" not in pool._POOL
|
||||
assert len(pool._POOL) <= pool._MAX_ENDPOINTS
|
||||
finally:
|
||||
for peer in peers:
|
||||
peer.close()
|
||||
pool.close_all()
|
||||
|
||||
def test_session_inner_message_strips_auth_fields():
|
||||
msg = {
|
||||
"id": "1", "command": "tabs.list", "args": {}, "user_agent": "browser-cli/1",
|
||||
|
||||
@@ -181,6 +181,23 @@ def test_rate_limiter_is_per_key():
|
||||
assert limiter.allow("a") is False
|
||||
assert limiter.allow("b") is False
|
||||
|
||||
def test_rate_limiter_caps_identity_buckets():
|
||||
limiter = RateLimiter(rate=0.0001, burst=1, max_buckets=3)
|
||||
for i in range(10):
|
||||
assert limiter.allow(f"key-{i}") is True
|
||||
assert len(limiter._buckets) <= 3
|
||||
|
||||
def test_rate_limiter_prunes_refilled_idle_buckets(monkeypatch):
|
||||
current = 1000.0
|
||||
monkeypatch.setattr("browser_cli.serve.security.time.monotonic", lambda: current)
|
||||
limiter = RateLimiter(rate=1, burst=2, max_buckets=2)
|
||||
assert limiter.allow("old") is True
|
||||
current += 120.0
|
||||
assert limiter.allow("a") is True
|
||||
assert limiter.allow("b") is True
|
||||
assert "old" not in limiter._buckets
|
||||
assert len(limiter._buckets) <= 2
|
||||
|
||||
# ── ServeSecurity ────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_effective_policy_prefers_per_key_override():
|
||||
|
||||
Reference in New Issue
Block a user