real-browser-cli 0.16.8__tar.gz → 0.16.9__tar.gz
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.
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/PKG-INFO +25 -1
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/README.md +24 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/navigate.py +8 -6
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/mcp/server.py +29 -7
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/navigation.py +35 -21
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/tabs.py +17 -1
- real_browser_cli-0.16.9/browser_cli/url_match.py +138 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/manifest.json +1 -1
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/pyproject.toml +1 -1
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_mcp.py +36 -1
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_new_feature_commands.py +44 -5
- real_browser_cli-0.16.9/tests/test_url_match.py +84 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/uv.lock +183 -160
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/.claude/settings.local.json +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/.gitea/workflows/package-extension.yml +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/.gitea/workflows/publish.yml +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/.gitea/workflows/testing.yml +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/.gitignore +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/.python-version +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/CLAUDE.md +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/LICENSE +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/PRIVACY.md +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/async_sdk.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/auth/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/auth/agent.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/auth/keys.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/auth/pq.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/auth/server_identity.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/auth/signing.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/cli.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/client/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/client/auth.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/client/core.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/client/messages.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/client/targets.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/command_security.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/auth.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/clients.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/completion.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/doctor.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/dom.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/events.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/extension.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/extract.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/groups.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/install.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/page.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/perf.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/raw.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/remote.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/common.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/labels.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/tabs_tree.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/rendering/windows_tree.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/script.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/search.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/serve.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/serve_http.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/session.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/storage.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/tabs.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/watch.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/windows.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/commands/workspace.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/compat/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/compat/auth.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/compat/commands.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/constants.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/endpoints.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/errors.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/framing.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/local_transport.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/markdown/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/markdown/html.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/markdown/render.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/mcp/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/mcp/naming.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/mcp/serialization.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/mcp/targets.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/models.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/native/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/native/host.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/native/local_server.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/native/protocol.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/platform.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/registry.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/remote/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/remote/auth.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/remote/known_hosts.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/remote/pool.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/remote/registry.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/remote/socket.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/remote/transport.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/base.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/browser_data.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/decorators.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/dom.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/extension.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/factories.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/groups.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/perf.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/routing.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/session.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/windows.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/sdk/workflow_decorators.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/search/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/search/engines.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/serve/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/serve/auth.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/serve/challenge.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/serve/control.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/serve/logging.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/serve/proxy.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/serve/runtime.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/serve/security.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/transport/__init__.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/transport/binary.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/transport/codecs.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/browser_cli/version_manager.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/examples/demo.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/examples/demo.sh +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/content.js +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/icon.svg +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/icons/icon-128.png +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/icons/icon-16.png +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/icons/icon-32.png +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/icons/icon-48.png +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/browser-api.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/classes/CommandGroup.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/classes/CommandRegistry.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/classes/JobManager.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/classes/NativeConnection.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/autosave.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/browser-data.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/dom.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/extension.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/groups.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/injected.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/navigation.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/perf.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/session-snapshot.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/session.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/tabs-query.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/tabs.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/commands/windows.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/dispatch.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/dom-ops.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/extract.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/markdown/code.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/markdown/index.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/markdown/renderer.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/markdown/root.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/markdown/utils.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content/page-info.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/content-dispatch.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/core/errors.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/core/group-helpers.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/core/index.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/core/scripting.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/core/storage.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/core/tab-groups.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/core/tab-helpers.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/core/throttle.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/index.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/types/command-args.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/types/index.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/types/jobs.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/types/json.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/types/messages.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/types/session.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/types/tabs.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/src/types/webextension.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/autosave.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/browser-api.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/chrome-mock.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/clients.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/jobs.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/navigation.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/paging.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/url-match.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/extension/test/windows.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/justfile +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/.claude/settings.local.json +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/.gitignore +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/README.md +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/credentials/BrowserCliApi.credentials.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/BrowserCli.node.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/browserCli.svg +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/protocol.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/request.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/nodes/BrowserCli/serveClient.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/package-lock.json +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/package.json +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/scripts/copy-assets.mjs +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/scripts/run-tests.mjs +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/test/protocol.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/test/request.test.ts +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/test-dist/protocol.test.mjs +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/test-dist/request.test.mjs +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/n8n-nodes-browser-cli/tsconfig.json +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/package-lock.json +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/package.json +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/scripts/gen_extension_key.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/scripts/package_extension.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/scripts/package_verified_crx.sh +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/scripts/setup_verified_crx_key.sh +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/shell.nix +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/conftest.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_api.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_auth.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_cli.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_client.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_commands_cli.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_compat.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_dom.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_extension_error_page_handling.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_extension_packaging.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_extract.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_framing.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_groups.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_known_hosts.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_markdown_security.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_native_host.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_nav.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_page.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_perf.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_performance_integration.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_platform.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_refactor_boundaries.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_registry.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_remote_pool.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_remote_protocol_matrix.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_remote_registry.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_rendering.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_serve.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_serve_security.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_session.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_storage.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_tabs.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_transport.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tests/test_windows.py +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/tsconfig.json +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/webstore-assets/screenshots/screenshot-1.png +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/webstore-assets/screenshots/screenshot-2.png +0 -0
- {real_browser_cli-0.16.8 → real_browser_cli-0.16.9}/webstore-assets/screenshots/screenshot-3.png +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: real-browser-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.9
|
|
4
4
|
Summary: Control your real running browser from the terminal or Python SDK
|
|
5
5
|
Project-URL: Homepage, https://git.yiprawr.dev/Automatisation/browser-cli
|
|
6
6
|
Project-URL: Repository, https://git.yiprawr.dev/Automatisation/browser-cli
|
|
@@ -275,6 +275,21 @@ optional `tab_id` and act on the active tab when it is omitted, so a caller
|
|
|
275
275
|
does not need a preceding `browser_tabs_list` round trip. `browser_tabs_close`
|
|
276
276
|
reports the tab it closed.
|
|
277
277
|
|
|
278
|
+
`browser_tabs_open` does not duplicate pages. It takes over an already-open tab
|
|
279
|
+
whose URL is nearly the same and reports `reused` next to the tab metadata, so
|
|
280
|
+
an agent that re-opens a page keeps working in the tab the user already has.
|
|
281
|
+
The `reuse` argument selects the match:
|
|
282
|
+
|
|
283
|
+
| `reuse` | Matches |
|
|
284
|
+
|---------|---------|
|
|
285
|
+
| `similar` (default) | Same site and page, ignoring fragment, trailing slash, query order, `www.` and default ports — plus any tab of the site when the requested URL is its root |
|
|
286
|
+
| `exact` | Only a character-for-character identical URL |
|
|
287
|
+
| `origin` | Any tab of the same site, whatever its path |
|
|
288
|
+
| `none` | Never reuse, always open a new tab |
|
|
289
|
+
|
|
290
|
+
A reused tab is only navigated when it is not already on the requested page, so
|
|
291
|
+
no reload throws away page state.
|
|
292
|
+
|
|
278
293
|
Available tools:
|
|
279
294
|
- `browser_tabs_list`, `browser_tabs_open`, `browser_tabs_close`
|
|
280
295
|
- `browser_navigate`, `browser_page_info`
|
|
@@ -340,6 +355,12 @@ browser-cli nav open https://example.com --focus # bring opened tab/w
|
|
|
340
355
|
browser-cli nav open https://example.com --window work # into a named window
|
|
341
356
|
browser-cli nav open https://example.com --group research # into a tab group (name or ID)
|
|
342
357
|
|
|
358
|
+
# Reuse an open tab instead of duplicating a page
|
|
359
|
+
browser-cli nav open https://example.com/app --reuse # nearly identical URL (fragment, trailing slash, query order, www)
|
|
360
|
+
browser-cli nav open https://example.com/app --reuse-exact # only a character-for-character match
|
|
361
|
+
browser-cli nav open https://example.com/app --reuse-domain # any tab of the same site
|
|
362
|
+
browser-cli nav open https://example.com/app --reuse-title "App" # tab whose title contains the text
|
|
363
|
+
|
|
343
364
|
# Reload
|
|
344
365
|
browser-cli nav reload # reload active tab
|
|
345
366
|
browser-cli nav reload 1234 # reload tab by ID
|
|
@@ -552,6 +573,9 @@ b.nav.search("google", "python asyncio")
|
|
|
552
573
|
tabs = b.tabs.list() # list[Tab]; in multi-browser mode each tab.browser is set
|
|
553
574
|
tab = b.tabs.open("https://example.com") # returns a bound Tab object
|
|
554
575
|
tab = b.tabs.open("https://example.com", wait=True, timeout=10)
|
|
576
|
+
tab = b.tabs.open("https://example.com/app", reuse=True) # reuse a tab with a nearly identical URL
|
|
577
|
+
tab = b.tabs.open("https://example.com/app", reuse="origin") # reuse any tab of that site
|
|
578
|
+
existing = b.nav.reusable_tab("https://example.com/app", reuse=True) # Tab or None, without touching the browser
|
|
555
579
|
active = b.tabs.active() # active Tab object
|
|
556
580
|
tab = b.tabs.get(1234) # tab by ID
|
|
557
581
|
tab = b.tabs.first("github") # first matching tab or None
|
|
@@ -179,6 +179,21 @@ optional `tab_id` and act on the active tab when it is omitted, so a caller
|
|
|
179
179
|
does not need a preceding `browser_tabs_list` round trip. `browser_tabs_close`
|
|
180
180
|
reports the tab it closed.
|
|
181
181
|
|
|
182
|
+
`browser_tabs_open` does not duplicate pages. It takes over an already-open tab
|
|
183
|
+
whose URL is nearly the same and reports `reused` next to the tab metadata, so
|
|
184
|
+
an agent that re-opens a page keeps working in the tab the user already has.
|
|
185
|
+
The `reuse` argument selects the match:
|
|
186
|
+
|
|
187
|
+
| `reuse` | Matches |
|
|
188
|
+
|---------|---------|
|
|
189
|
+
| `similar` (default) | Same site and page, ignoring fragment, trailing slash, query order, `www.` and default ports — plus any tab of the site when the requested URL is its root |
|
|
190
|
+
| `exact` | Only a character-for-character identical URL |
|
|
191
|
+
| `origin` | Any tab of the same site, whatever its path |
|
|
192
|
+
| `none` | Never reuse, always open a new tab |
|
|
193
|
+
|
|
194
|
+
A reused tab is only navigated when it is not already on the requested page, so
|
|
195
|
+
no reload throws away page state.
|
|
196
|
+
|
|
182
197
|
Available tools:
|
|
183
198
|
- `browser_tabs_list`, `browser_tabs_open`, `browser_tabs_close`
|
|
184
199
|
- `browser_navigate`, `browser_page_info`
|
|
@@ -244,6 +259,12 @@ browser-cli nav open https://example.com --focus # bring opened tab/w
|
|
|
244
259
|
browser-cli nav open https://example.com --window work # into a named window
|
|
245
260
|
browser-cli nav open https://example.com --group research # into a tab group (name or ID)
|
|
246
261
|
|
|
262
|
+
# Reuse an open tab instead of duplicating a page
|
|
263
|
+
browser-cli nav open https://example.com/app --reuse # nearly identical URL (fragment, trailing slash, query order, www)
|
|
264
|
+
browser-cli nav open https://example.com/app --reuse-exact # only a character-for-character match
|
|
265
|
+
browser-cli nav open https://example.com/app --reuse-domain # any tab of the same site
|
|
266
|
+
browser-cli nav open https://example.com/app --reuse-title "App" # tab whose title contains the text
|
|
267
|
+
|
|
247
268
|
# Reload
|
|
248
269
|
browser-cli nav reload # reload active tab
|
|
249
270
|
browser-cli nav reload 1234 # reload tab by ID
|
|
@@ -456,6 +477,9 @@ b.nav.search("google", "python asyncio")
|
|
|
456
477
|
tabs = b.tabs.list() # list[Tab]; in multi-browser mode each tab.browser is set
|
|
457
478
|
tab = b.tabs.open("https://example.com") # returns a bound Tab object
|
|
458
479
|
tab = b.tabs.open("https://example.com", wait=True, timeout=10)
|
|
480
|
+
tab = b.tabs.open("https://example.com/app", reuse=True) # reuse a tab with a nearly identical URL
|
|
481
|
+
tab = b.tabs.open("https://example.com/app", reuse="origin") # reuse any tab of that site
|
|
482
|
+
existing = b.nav.reusable_tab("https://example.com/app", reuse=True) # Tab or None, without touching the browser
|
|
459
483
|
active = b.tabs.active() # active Tab object
|
|
460
484
|
tab = b.tabs.get(1234) # tab by ID
|
|
461
485
|
tab = b.tabs.first("github") # first matching tab or None
|
|
@@ -13,13 +13,14 @@ def nav_group():
|
|
|
13
13
|
@click.option("--focus", is_flag=True, help="Bring the opened tab/window to the front")
|
|
14
14
|
@click.option("--window", "window_name", default=None, help="Open in named window")
|
|
15
15
|
@click.option("--group", "group_name", default=None, help="Open directly into a tab group (name or ID)")
|
|
16
|
-
@click.option("--reuse", is_flag=True, help="Reuse an existing tab with
|
|
16
|
+
@click.option("--reuse", is_flag=True, help="Reuse an existing tab with a nearly identical URL (ignores fragment, trailing slash, query order)")
|
|
17
|
+
@click.option("--reuse-exact", is_flag=True, help="Reuse an existing tab only when its URL matches character for character")
|
|
17
18
|
@click.option("--reuse-domain", is_flag=True, help="Reuse an existing tab with the same domain")
|
|
18
19
|
@click.option("--reuse-title", default=None, metavar="TEXT", help="Reuse an existing tab whose title contains TEXT")
|
|
19
20
|
@handle_errors
|
|
20
|
-
def cmd_open(url, focus, window_name, group_name, reuse, reuse_domain, reuse_title):
|
|
21
|
+
def cmd_open(url, focus, window_name, group_name, reuse, reuse_exact, reuse_domain, reuse_title):
|
|
21
22
|
"""Open URL in a new tab without stealing focus by default."""
|
|
22
|
-
client_from_ctx().nav.open(url, focus=focus, window=window_name, group=group_name, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
|
|
23
|
+
client_from_ctx().nav.open(url, focus=focus, window=window_name, group=group_name, reuse="exact" if reuse_exact else reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
|
|
23
24
|
suffix = ""
|
|
24
25
|
if group_name:
|
|
25
26
|
suffix = f" in group '{group_name}'"
|
|
@@ -76,13 +77,14 @@ def cmd_focus(pattern):
|
|
|
76
77
|
@click.option("--focus", is_flag=True, help="Bring the opened tab/window to the front")
|
|
77
78
|
@click.option("--window", "window_name", default=None, help="Open in named window")
|
|
78
79
|
@click.option("--group", "group_name", default=None, help="Open in tab group")
|
|
79
|
-
@click.option("--reuse", is_flag=True, help="Reuse an existing tab with
|
|
80
|
+
@click.option("--reuse", is_flag=True, help="Reuse an existing tab with a nearly identical URL (ignores fragment, trailing slash, query order)")
|
|
81
|
+
@click.option("--reuse-exact", is_flag=True, help="Reuse an existing tab only when its URL matches character for character")
|
|
80
82
|
@click.option("--reuse-domain", is_flag=True, help="Reuse an existing tab with the same domain")
|
|
81
83
|
@click.option("--reuse-title", default=None, metavar="TEXT", help="Reuse an existing tab whose title contains TEXT")
|
|
82
84
|
@handle_errors
|
|
83
|
-
def cmd_open_wait(url, timeout, focus, window_name, group_name, reuse, reuse_domain, reuse_title):
|
|
85
|
+
def cmd_open_wait(url, timeout, focus, window_name, group_name, reuse, reuse_exact, reuse_domain, reuse_title):
|
|
84
86
|
"""Open URL in a new tab and wait until fully loaded."""
|
|
85
|
-
tab = client_from_ctx().nav.open_wait(url, timeout=timeout, focus=focus, window=window_name, group=group_name, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
|
|
87
|
+
tab = client_from_ctx().nav.open_wait(url, timeout=timeout, focus=focus, window=window_name, group=group_name, reuse="exact" if reuse_exact else reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
|
|
86
88
|
console.print(f"[green]Loaded:[/green] {url}" + (f" — {tab.title}" if tab.title else ""))
|
|
87
89
|
|
|
88
90
|
@nav_group.command("wait")
|
|
@@ -12,12 +12,12 @@ import json
|
|
|
12
12
|
import os
|
|
13
13
|
from collections.abc import Callable
|
|
14
14
|
from typing import Any, Literal
|
|
15
|
-
from urllib.parse import urlsplit
|
|
16
15
|
|
|
17
16
|
from browser_cli import BrowserCLI
|
|
18
17
|
from browser_cli.mcp.naming import resolve_tool_prefix, tool_name
|
|
19
18
|
from browser_cli.mcp.serialization import structured
|
|
20
19
|
from browser_cli.mcp.targets import resolve_tab_id
|
|
20
|
+
from browser_cli.url_match import find_reusable_tab, needs_navigation, tab_id as read_tab_id, tab_url
|
|
21
21
|
|
|
22
22
|
ClientFactory = Callable[..., BrowserCLI]
|
|
23
23
|
|
|
@@ -25,6 +25,11 @@ _SERVER_INSTRUCTIONS = """Control a real, user-visible browser through browser-c
|
|
|
25
25
|
The server is stateless: pass browser, remote, and key on each tool call when a
|
|
26
26
|
specific target is required. Tool calls affect the user's actual browser. Read
|
|
27
27
|
current tabs/page state instead of assuming IDs or content from an earlier call.
|
|
28
|
+
Reuse before opening: tabs_open takes over an already-open tab whose URL is
|
|
29
|
+
nearly the same (same site and page, or any tab of that site when you ask for
|
|
30
|
+
its root) and reports reused=true with that tab's ID. Pass reuse="none" only
|
|
31
|
+
when a separate tab is really wanted, and reuse="origin" to take over any tab
|
|
32
|
+
of the site. Use tabs_list(search=...) first when you want to pick the tab.
|
|
28
33
|
"""
|
|
29
34
|
|
|
30
35
|
def _client(factory: ClientFactory, browser: str | None, remote: str | None, key: str | None) -> BrowserCLI:
|
|
@@ -74,7 +79,10 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
|
|
|
74
79
|
remote: str | None = None,
|
|
75
80
|
key: str | None = None,
|
|
76
81
|
) -> list[dict[str, Any]]:
|
|
77
|
-
"""List current tabs, optionally filtering by URL or title substring.
|
|
82
|
+
"""List current tabs, optionally filtering by URL or title substring.
|
|
83
|
+
|
|
84
|
+
Check this before opening anything so an already-open page gets reused.
|
|
85
|
+
"""
|
|
78
86
|
tabs = structured(_client(client_factory, browser, remote, key).tabs.list())
|
|
79
87
|
if not search:
|
|
80
88
|
return tabs
|
|
@@ -92,15 +100,29 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
|
|
|
92
100
|
timeout: float = 30.0,
|
|
93
101
|
background: bool = False,
|
|
94
102
|
focus: bool = False,
|
|
103
|
+
reuse: Literal["similar", "exact", "origin", "none"] = "similar",
|
|
95
104
|
browser: str | None = None,
|
|
96
105
|
remote: str | None = None,
|
|
97
106
|
key: str | None = None,
|
|
98
107
|
) -> dict[str, Any]:
|
|
99
|
-
"""Open a URL in
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
"""Open a URL in the real browser, reusing an already-open tab with a nearly identical URL.
|
|
109
|
+
|
|
110
|
+
Returns the tab metadata plus ``reused``. ``reuse`` selects the match:
|
|
111
|
+
``similar`` (default: same site and page, or any tab of that site when the
|
|
112
|
+
requested URL is its root), ``exact``, ``origin`` (any tab of the site), or
|
|
113
|
+
``none`` to always create a new tab.
|
|
114
|
+
"""
|
|
115
|
+
client = _client(client_factory, browser, remote, key)
|
|
116
|
+
existing = find_reusable_tab(structured(client.tabs.list()), url, reuse)
|
|
117
|
+
if existing is not None:
|
|
118
|
+
target = read_tab_id(existing)
|
|
119
|
+
if needs_navigation(tab_url(existing), url):
|
|
120
|
+
client.nav.to(target, url)
|
|
121
|
+
if focus:
|
|
122
|
+
client.tabs.activate(target)
|
|
123
|
+
return {**structured(client.tabs.status(target)), "reused": True}
|
|
124
|
+
tab = client.tabs.open(url, wait=wait, timeout=timeout, background=background, focus=focus)
|
|
125
|
+
return {**structured(tab), "reused": False}
|
|
104
126
|
|
|
105
127
|
@mcp.tool(name=tool_name("tabs_close", prefix))
|
|
106
128
|
def tabs_close(
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
from browser_cli.models import Tab
|
|
5
5
|
from browser_cli.sdk.base import Namespace, sdk_command
|
|
6
|
+
from browser_cli.url_match import find_reusable_tab, needs_navigation, normalize_mode
|
|
6
7
|
|
|
7
8
|
def _open_args(self, url, *, background=False, focus=False, window=None, group=None, **_ignored):
|
|
8
9
|
return {"url": url, "background": background or not focus, "focus": focus, "window": window, "group": group}
|
|
@@ -21,18 +22,20 @@ class NavigationNS(Namespace):
|
|
|
21
22
|
focus: bool = False,
|
|
22
23
|
window: str | None = None,
|
|
23
24
|
group: str | None = None,
|
|
24
|
-
reuse: bool = False,
|
|
25
|
+
reuse: bool | str = False,
|
|
25
26
|
reuse_domain: bool = False,
|
|
26
27
|
reuse_title: str | None = None,
|
|
27
28
|
) -> None:
|
|
28
29
|
"""Open *url* in a new tab without stealing OS focus by default.
|
|
29
30
|
|
|
30
31
|
``reuse``/``reuse_domain``/``reuse_title`` navigate an existing matching tab
|
|
31
|
-
instead of creating a new one.
|
|
32
|
+
instead of creating a new one. ``reuse=True`` matches nearly identical URLs;
|
|
33
|
+
pass ``reuse='exact'`` or ``reuse='origin'`` to tighten or widen that.
|
|
32
34
|
"""
|
|
33
|
-
tab = self.
|
|
35
|
+
tab = self.reusable_tab(url, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
|
|
34
36
|
if tab is not None:
|
|
35
|
-
|
|
37
|
+
if needs_navigation(tab.url, url):
|
|
38
|
+
self.to(tab.id, url)
|
|
36
39
|
if focus:
|
|
37
40
|
self._c.tabs.activate(tab.id)
|
|
38
41
|
return None
|
|
@@ -48,14 +51,15 @@ class NavigationNS(Namespace):
|
|
|
48
51
|
focus: bool = False,
|
|
49
52
|
window: str | None = None,
|
|
50
53
|
group: str | None = None,
|
|
51
|
-
reuse: bool = False,
|
|
54
|
+
reuse: bool | str = False,
|
|
52
55
|
reuse_domain: bool = False,
|
|
53
56
|
reuse_title: str | None = None,
|
|
54
57
|
) -> Tab:
|
|
55
58
|
"""Open *url* in a new tab and block until fully loaded. Returns the Tab."""
|
|
56
|
-
tab = self.
|
|
59
|
+
tab = self.reusable_tab(url, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
|
|
57
60
|
if tab is not None:
|
|
58
|
-
|
|
61
|
+
if needs_navigation(tab.url, url):
|
|
62
|
+
self.to(tab.id, url)
|
|
59
63
|
if focus:
|
|
60
64
|
self._c.tabs.activate(tab.id)
|
|
61
65
|
return self._c.tabs.wait_for_load(tab.id, timeout=timeout)
|
|
@@ -91,21 +95,31 @@ class NavigationNS(Namespace):
|
|
|
91
95
|
def to(self, tab_id: int, url: str) -> None:
|
|
92
96
|
"""Navigate a specific tab to *url* in place."""
|
|
93
97
|
|
|
94
|
-
def
|
|
95
|
-
|
|
98
|
+
def reusable_tab(
|
|
99
|
+
self,
|
|
100
|
+
url: str,
|
|
101
|
+
*,
|
|
102
|
+
reuse: bool | str = False,
|
|
103
|
+
reuse_domain: bool = False,
|
|
104
|
+
reuse_title: str | None = None,
|
|
105
|
+
) -> Tab | None:
|
|
106
|
+
"""Return the open tab that already fits *url*, or ``None``.
|
|
107
|
+
|
|
108
|
+
``reuse`` takes ``True`` (nearly identical URL), ``'exact'``, ``'similar'``,
|
|
109
|
+
``'origin'`` or ``'none'``; ``reuse_title`` falls back to a title substring.
|
|
110
|
+
"""
|
|
111
|
+
mode = normalize_mode(reuse, reuse_domain)
|
|
112
|
+
if mode == "none" and not reuse_title:
|
|
96
113
|
return None
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return tab
|
|
107
|
-
if reuse_title and reuse_title.lower() in (tab.title or "").lower():
|
|
108
|
-
return tab
|
|
114
|
+
tabs = self._c.tabs.list()
|
|
115
|
+
match = find_reusable_tab(tabs, url, mode)
|
|
116
|
+
if match is not None:
|
|
117
|
+
return match
|
|
118
|
+
if reuse_title:
|
|
119
|
+
needle = reuse_title.lower()
|
|
120
|
+
for tab in tabs:
|
|
121
|
+
if needle in (tab.title or "").lower():
|
|
122
|
+
return tab
|
|
109
123
|
return None
|
|
110
124
|
|
|
111
125
|
def search(
|
|
@@ -5,6 +5,7 @@ from collections.abc import Callable, Iterable
|
|
|
5
5
|
|
|
6
6
|
from browser_cli.models import BrowserCounts, Tab
|
|
7
7
|
from browser_cli.sdk.base import Namespace
|
|
8
|
+
from browser_cli.url_match import needs_navigation
|
|
8
9
|
|
|
9
10
|
# Keep SDK-driven bulk closes comfortably below the native-host response
|
|
10
11
|
# timeout. The extension can close larger batches, but real browsers may take
|
|
@@ -32,14 +33,29 @@ class TabsNS(Namespace):
|
|
|
32
33
|
focus: bool = False,
|
|
33
34
|
window: str | None = None,
|
|
34
35
|
group: str | None = None,
|
|
36
|
+
reuse: bool | str = False,
|
|
37
|
+
reuse_domain: bool = False,
|
|
38
|
+
reuse_title: str | None = None,
|
|
35
39
|
) -> Tab:
|
|
36
40
|
"""Open *url* in a new tab and return a bound :class:`Tab`.
|
|
37
41
|
|
|
38
42
|
Set ``wait=True`` to block until the page reaches ``readyState=complete``.
|
|
39
43
|
Pass ``focus=True`` to explicitly bring the created tab/window forward.
|
|
44
|
+
``reuse=True`` navigates an already-open tab with a nearly identical URL
|
|
45
|
+
instead of creating a duplicate.
|
|
40
46
|
"""
|
|
41
47
|
if wait:
|
|
42
|
-
return self._c.nav.open_wait(
|
|
48
|
+
return self._c.nav.open_wait(
|
|
49
|
+
url, timeout=timeout, background=background, focus=focus, window=window, group=group,
|
|
50
|
+
reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title,
|
|
51
|
+
)
|
|
52
|
+
existing = self._c.nav.reusable_tab(url, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
|
|
53
|
+
if existing is not None:
|
|
54
|
+
if needs_navigation(existing.url, url):
|
|
55
|
+
self._c.nav.to(existing.id, url)
|
|
56
|
+
if focus:
|
|
57
|
+
self.activate(existing.id)
|
|
58
|
+
return self.status(existing.id)
|
|
43
59
|
return self.require_tab(
|
|
44
60
|
self.command("navigate.open", {"url": url, "background": background or not focus, "focus": focus, "window": window, "group": group}),
|
|
45
61
|
"navigate.open returned unexpected data",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Match a requested URL against already-open tabs.
|
|
2
|
+
|
|
3
|
+
Agents and scripts usually want the tab that already shows a page instead of a
|
|
4
|
+
duplicate of it. These helpers answer "is this tab already (nearly) the URL I
|
|
5
|
+
am about to open?" without touching the browser.
|
|
6
|
+
|
|
7
|
+
Modes:
|
|
8
|
+
``none`` never reuse a tab
|
|
9
|
+
``exact`` only a byte-identical URL
|
|
10
|
+
``similar`` same site and page, ignoring fragment, trailing slash, query
|
|
11
|
+
order, ``www.`` and default ports; a request for the site root
|
|
12
|
+
also matches any tab on that site
|
|
13
|
+
``origin`` any tab on the same site, whatever its path
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any, Literal
|
|
19
|
+
from urllib.parse import parse_qsl, urlsplit
|
|
20
|
+
|
|
21
|
+
ReuseMode = Literal["none", "exact", "similar", "origin"]
|
|
22
|
+
|
|
23
|
+
REUSE_MODES: tuple[str, ...] = ("none", "exact", "similar", "origin")
|
|
24
|
+
|
|
25
|
+
_DEFAULT_PORTS = {"http": 80, "https": 443, "ws": 80, "wss": 443}
|
|
26
|
+
_ROOT_PATHS = {"", "/"}
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class UrlKey:
|
|
30
|
+
"""Canonical, comparable form of a URL."""
|
|
31
|
+
|
|
32
|
+
scheme: str
|
|
33
|
+
host: str
|
|
34
|
+
port: int | None
|
|
35
|
+
path: str
|
|
36
|
+
query: tuple[tuple[str, str], ...]
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def origin(self) -> tuple[str, str, int | None]:
|
|
40
|
+
return (self.scheme, self.host, self.port)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_root(self) -> bool:
|
|
44
|
+
return self.path == "" and not self.query
|
|
45
|
+
|
|
46
|
+
def url_key(url: str | None) -> UrlKey | None:
|
|
47
|
+
"""Return the canonical key of *url*, or ``None`` when it is not comparable."""
|
|
48
|
+
if not url:
|
|
49
|
+
return None
|
|
50
|
+
parts = urlsplit(url.strip())
|
|
51
|
+
if not parts.scheme or not parts.hostname:
|
|
52
|
+
return None
|
|
53
|
+
scheme = parts.scheme.lower()
|
|
54
|
+
host = parts.hostname.lower()
|
|
55
|
+
if host.startswith("www.") and len(host) > 4:
|
|
56
|
+
host = host[4:]
|
|
57
|
+
port = parts.port
|
|
58
|
+
if port is not None and _DEFAULT_PORTS.get(scheme) == port:
|
|
59
|
+
port = None
|
|
60
|
+
path = parts.path.rstrip("/")
|
|
61
|
+
query = tuple(sorted(parse_qsl(parts.query, keep_blank_values=True)))
|
|
62
|
+
return UrlKey(scheme=scheme, host=host, port=port, path=path, query=query)
|
|
63
|
+
|
|
64
|
+
def normalize_mode(reuse: bool | str | None, reuse_domain: bool = False) -> ReuseMode:
|
|
65
|
+
"""Fold the boolean flags and explicit mode names into one :data:`ReuseMode`."""
|
|
66
|
+
if reuse_domain:
|
|
67
|
+
return "origin"
|
|
68
|
+
if reuse is True:
|
|
69
|
+
return "similar"
|
|
70
|
+
if reuse is False or reuse is None:
|
|
71
|
+
return "none"
|
|
72
|
+
mode = str(reuse).lower()
|
|
73
|
+
if mode not in REUSE_MODES:
|
|
74
|
+
raise ValueError(f"Unknown reuse mode '{reuse}'. Use one of: {', '.join(REUSE_MODES)}")
|
|
75
|
+
return mode # type: ignore[return-value]
|
|
76
|
+
|
|
77
|
+
def urls_match(url: str, candidate: str | None, mode: ReuseMode) -> bool:
|
|
78
|
+
"""Report whether *candidate* may be reused for *url* under *mode*."""
|
|
79
|
+
if mode == "none" or not candidate:
|
|
80
|
+
return False
|
|
81
|
+
if mode == "exact":
|
|
82
|
+
return candidate == url
|
|
83
|
+
wanted, other = url_key(url), url_key(candidate)
|
|
84
|
+
if wanted is None or other is None:
|
|
85
|
+
return False
|
|
86
|
+
if wanted.origin != other.origin:
|
|
87
|
+
return False
|
|
88
|
+
if mode == "origin":
|
|
89
|
+
return True
|
|
90
|
+
return wanted.path == other.path or wanted.is_root
|
|
91
|
+
|
|
92
|
+
def needs_navigation(current: str | None, url: str) -> bool:
|
|
93
|
+
"""Report whether a reused tab still has to be navigated to *url*."""
|
|
94
|
+
if current == url:
|
|
95
|
+
return False
|
|
96
|
+
wanted, other = url_key(url), url_key(current)
|
|
97
|
+
return wanted is None or other is None or wanted != other
|
|
98
|
+
|
|
99
|
+
def tab_url(tab: Any) -> str | None:
|
|
100
|
+
"""Read a tab URL from an SDK ``Tab`` or a serialized tab dict."""
|
|
101
|
+
if isinstance(tab, dict):
|
|
102
|
+
value = tab.get("url")
|
|
103
|
+
else:
|
|
104
|
+
value = getattr(tab, "url", None)
|
|
105
|
+
return value if isinstance(value, str) else None
|
|
106
|
+
|
|
107
|
+
def tab_id(tab: Any) -> int | None:
|
|
108
|
+
"""Read a tab ID from an SDK ``Tab`` or a serialized tab dict."""
|
|
109
|
+
value = tab.get("id") if isinstance(tab, dict) else getattr(tab, "id", None)
|
|
110
|
+
return value if isinstance(value, int) else None
|
|
111
|
+
|
|
112
|
+
def find_reusable_tab(tabs: Any, url: str, mode: ReuseMode):
|
|
113
|
+
"""Return the open tab that best fits *url*, or ``None``.
|
|
114
|
+
|
|
115
|
+
Closer matches win: an identical URL beats a canonical match, which beats a
|
|
116
|
+
looser site-level match; among equally loose matches the tab closest to the
|
|
117
|
+
requested page wins.
|
|
118
|
+
"""
|
|
119
|
+
if mode == "none":
|
|
120
|
+
return None
|
|
121
|
+
wanted = url_key(url)
|
|
122
|
+
best, best_rank = None, None
|
|
123
|
+
for tab in tabs or ():
|
|
124
|
+
candidate = tab_url(tab)
|
|
125
|
+
if not urls_match(url, candidate, mode):
|
|
126
|
+
continue
|
|
127
|
+
rank = _rank(wanted, candidate)
|
|
128
|
+
if best_rank is None or rank < best_rank:
|
|
129
|
+
best, best_rank = tab, rank
|
|
130
|
+
return best
|
|
131
|
+
|
|
132
|
+
def _rank(wanted: UrlKey | None, candidate: str | None) -> tuple[int, int]:
|
|
133
|
+
"""Lower is a closer match: exact string, canonical, then shallowest path."""
|
|
134
|
+
other = url_key(candidate)
|
|
135
|
+
if wanted is not None and other is not None and wanted == other:
|
|
136
|
+
return (0, 0)
|
|
137
|
+
depth = other.path.count("/") if other is not None else 99
|
|
138
|
+
return (1, depth)
|
|
@@ -27,6 +27,7 @@ class FakeClient:
|
|
|
27
27
|
open=self.tabs_open,
|
|
28
28
|
close=self.tabs_close,
|
|
29
29
|
active=self.tabs_active,
|
|
30
|
+
activate=self.tabs_activate,
|
|
30
31
|
status=self.tabs_status,
|
|
31
32
|
screenshot=self.tabs_screenshot,
|
|
32
33
|
)
|
|
@@ -54,6 +55,9 @@ class FakeClient:
|
|
|
54
55
|
self.calls.append(("active",))
|
|
55
56
|
return SimpleNamespace(id=7)
|
|
56
57
|
|
|
58
|
+
def tabs_activate(self, tab_id):
|
|
59
|
+
self.calls.append(("activate", tab_id))
|
|
60
|
+
|
|
57
61
|
def tabs_status(self, tab_id):
|
|
58
62
|
self.calls.append(("status", tab_id))
|
|
59
63
|
return {"id": tab_id, "title": "Navigated", "url": "https://example.com/next"}
|
|
@@ -207,6 +211,7 @@ async def test_mutating_tools_use_sdk_and_return_fresh_state(client):
|
|
|
207
211
|
"url": "https://example.com",
|
|
208
212
|
"wait": True,
|
|
209
213
|
"focus": True,
|
|
214
|
+
"reuse": "none",
|
|
210
215
|
})
|
|
211
216
|
navigated = await client.call_tool("browser_navigate", {
|
|
212
217
|
"tab_id": 8,
|
|
@@ -216,7 +221,7 @@ async def test_mutating_tools_use_sdk_and_return_fresh_state(client):
|
|
|
216
221
|
typed = await client.call_tool("browser_dom_type", {"selector": "#name", "text": "Daniel"})
|
|
217
222
|
|
|
218
223
|
assert opened.structured_content == {
|
|
219
|
-
"id": 8, "title": "Opened", "url": "https://example.com"
|
|
224
|
+
"id": 8, "title": "Opened", "url": "https://example.com", "reused": False
|
|
220
225
|
}
|
|
221
226
|
assert navigated.structured_content["id"] == 8
|
|
222
227
|
assert clicked.structured_content["url"] == "https://example.com"
|
|
@@ -230,6 +235,36 @@ async def test_mutating_tools_use_sdk_and_return_fresh_state(client):
|
|
|
230
235
|
("navigate", 8, "https://example.com/next"), ("status", 8)
|
|
231
236
|
]
|
|
232
237
|
|
|
238
|
+
@pytest.mark.anyio
|
|
239
|
+
async def test_tabs_open_reuses_a_tab_that_already_shows_the_page(client):
|
|
240
|
+
opened = await client.call_tool("browser_tabs_open", {"url": "https://www.example.com/#top"})
|
|
241
|
+
|
|
242
|
+
assert opened.structured_content == {
|
|
243
|
+
"id": 7, "title": "Navigated", "url": "https://example.com/next", "reused": True
|
|
244
|
+
}
|
|
245
|
+
assert FakeClient.instances[0].calls == [("status", 7)] # already there → no navigation
|
|
246
|
+
|
|
247
|
+
@pytest.mark.anyio
|
|
248
|
+
async def test_tabs_open_reuses_a_site_tab_and_navigates_it(client):
|
|
249
|
+
opened = await client.call_tool(
|
|
250
|
+
"browser_tabs_open",
|
|
251
|
+
{"url": "https://browser-host.example/app?view=logs", "focus": True},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
assert opened.structured_content["reused"] is True
|
|
255
|
+
assert FakeClient.instances[0].calls == [
|
|
256
|
+
("navigate", 9, "https://browser-host.example/app?view=logs"),
|
|
257
|
+
("activate", 9),
|
|
258
|
+
("status", 9),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
@pytest.mark.anyio
|
|
262
|
+
async def test_tabs_open_does_not_reuse_an_unrelated_tab(client):
|
|
263
|
+
opened = await client.call_tool("browser_tabs_open", {"url": "https://other.example/docs"})
|
|
264
|
+
|
|
265
|
+
assert opened.structured_content["reused"] is False
|
|
266
|
+
assert FakeClient.instances[0].calls[0][0] == "open"
|
|
267
|
+
|
|
233
268
|
@pytest.mark.anyio
|
|
234
269
|
async def test_tab_tools_default_to_the_active_tab(client):
|
|
235
270
|
navigated = await client.call_tool("browser_navigate", {"url": "https://example.com/next"})
|
|
@@ -34,21 +34,60 @@ def test_session_export_cli_prints_json():
|
|
|
34
34
|
assert result.exit_code == 0
|
|
35
35
|
assert '"name": "work"' in result.output
|
|
36
36
|
|
|
37
|
-
def
|
|
38
|
-
|
|
37
|
+
def _reuse_sender(calls, tab_url="https://example.com"):
|
|
38
|
+
tab = {"id": 7, "windowId": 1, "active": False, "muted": False, "title": "Example", "url": tab_url}
|
|
39
39
|
|
|
40
40
|
def sender(command, args=None, **kwargs):
|
|
41
41
|
calls.append((command, args))
|
|
42
42
|
if command == "tabs.list":
|
|
43
|
-
return [
|
|
43
|
+
return [tab]
|
|
44
|
+
if command in {"tabs.status", "navigate.open"}:
|
|
45
|
+
return dict(tab)
|
|
44
46
|
return {}
|
|
47
|
+
return sender
|
|
48
|
+
|
|
49
|
+
def test_nav_open_reuse_navigates_existing_tab_instead_of_opening_new():
|
|
50
|
+
calls = []
|
|
45
51
|
|
|
46
|
-
BrowserCLI(browser="testing", _command_sender=
|
|
52
|
+
BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls, "https://example.com/app?a=1")).nav.open(
|
|
53
|
+
"https://example.com/app?a=2", reuse=True
|
|
54
|
+
)
|
|
47
55
|
assert calls == [
|
|
48
56
|
("tabs.list", {}),
|
|
49
|
-
("navigate.to", {"tabId": 7, "url": "https://example.com"}),
|
|
57
|
+
("navigate.to", {"tabId": 7, "url": "https://example.com/app?a=2"}),
|
|
50
58
|
]
|
|
51
59
|
|
|
60
|
+
def test_nav_open_reuse_matches_nearly_identical_urls():
|
|
61
|
+
calls = []
|
|
62
|
+
|
|
63
|
+
BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls, "https://example.com/app?a=1&b=2")).nav.open(
|
|
64
|
+
"https://www.example.com/app/?b=2&a=1#top", reuse=True
|
|
65
|
+
)
|
|
66
|
+
assert calls == [("tabs.list", {})] # same page already open → no reload, no new tab
|
|
67
|
+
|
|
68
|
+
def test_nav_open_reuse_exact_opens_a_new_tab_for_a_different_url():
|
|
69
|
+
calls = []
|
|
70
|
+
|
|
71
|
+
BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls)).nav.open(
|
|
72
|
+
"https://example.com/", reuse="exact"
|
|
73
|
+
)
|
|
74
|
+
assert [command for command, _ in calls] == ["tabs.list", "navigate.open"]
|
|
75
|
+
|
|
76
|
+
def test_tabs_open_reuse_returns_the_existing_tab():
|
|
77
|
+
calls = []
|
|
78
|
+
|
|
79
|
+
tab = BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls)).tabs.open(
|
|
80
|
+
"https://example.com/deep/page", reuse="origin"
|
|
81
|
+
)
|
|
82
|
+
assert tab.id == 7
|
|
83
|
+
assert [command for command, _ in calls] == ["tabs.list", "navigate.to", "tabs.status"]
|
|
84
|
+
|
|
85
|
+
def test_tabs_open_without_reuse_always_creates_a_tab():
|
|
86
|
+
calls = []
|
|
87
|
+
|
|
88
|
+
BrowserCLI(browser="testing", _command_sender=_reuse_sender(calls)).tabs.open("https://example.com")
|
|
89
|
+
assert [command for command, _ in calls] == ["navigate.open"]
|
|
90
|
+
|
|
52
91
|
def _tree_sender(tabs, groups):
|
|
53
92
|
def sender(command, args=None, **kwargs):
|
|
54
93
|
if command == "tabs.list":
|