mirror of
https://github.com/feder-cr/Jobs_Applier_AI_Agent.git
synced 2026-11-07 02:35:57 -10:00
dots: invisible-playwright-mcp's interface under its own name
The command is the pinned package's ui, reached through its command group so the .env beside it is read. Nothing of the loop, the browser or the page is re-implemented here.
This commit is contained in:
commit
cbbe1bc9c2
10 files changed
+306
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
# The pre-push hook is a shell script, and git must never hand it to a POSIX
|
||||||
|
# shell with CRLF line endings: `/bin/sh^M: bad interpreter` is how a gate stops
|
||||||
|
# running on a machine where nothing else looks wrong. Git-for-Windows' sh is
|
||||||
|
# the one that runs it on a Windows clone with core.autocrlf=true.
|
||||||
|
.githooks/** text eol=lf
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Pre-push gate. THE POLICY IS NOT IN THIS FILE - it is `invisible_core.hooks`,
|
||||||
|
# and this stub exists only to find an interpreter and hand over stdin.
|
||||||
|
#
|
||||||
|
# It used to be here, in three repositories, as 743 lines of shell that were 207
|
||||||
|
# identical lines out of 211 between two of them. Nine of the eleven differences
|
||||||
|
# were accidental: the core ran no pytest, the core set a variable three times
|
||||||
|
# that only the other two ever read, the core resolved an interpreter and then
|
||||||
|
# called bare `python` anyway. Every fix landed in whichever copy was open.
|
||||||
|
#
|
||||||
|
# What this repository wants gated is declared in its own pyproject, under
|
||||||
|
# [tool.invisible.hooks]. That is the only per-repo part, and it is data.
|
||||||
|
#
|
||||||
|
# Install once with:
|
||||||
|
# git config core.hooksPath .githooks
|
||||||
|
# A test in this repo fails in an uninstalled checkout, so nobody has to
|
||||||
|
# remember. Bypass a known-broken WIP push with `git push --no-verify`; that is
|
||||||
|
# the only escape hatch, and never for a branch that feeds a release.
|
||||||
|
|
||||||
|
# No `set -e`: exit codes here have to be READ and explained, not turned into a
|
||||||
|
# silent abort. A gate that dies without a message is the worst shape of all.
|
||||||
|
|
||||||
|
# Run from the repository root regardless of where `git push` was invoked.
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
|
python_bin=""
|
||||||
|
for candidate in "$INVISIBLE_GATE_PYTHON" python3 python py; do
|
||||||
|
[ -n "$candidate" ] || continue
|
||||||
|
if command -v "$candidate" >/dev/null 2>&1; then
|
||||||
|
python_bin="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$python_bin" ]; then
|
||||||
|
echo "[pre-push] REFUSED - no python on PATH, so no gate can run." >&2
|
||||||
|
echo "[pre-push] Nothing must be pushed past a hook that could not check it." >&2
|
||||||
|
echo "[pre-push] Set INVISIBLE_GATE_PYTHON to an interpreter and push again." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `src` first so a checkout gates itself with the policy in the tree being
|
||||||
|
# pushed. os.getcwd() rather than $PWD on purpose: under git-bash on Windows
|
||||||
|
# $PWD is /c/... and Python does not resolve that mount.
|
||||||
|
# exec, so stdin (the refs git is pushing) reaches the policy untouched.
|
||||||
|
exec "$python_bin" -c 'import os, sys
|
||||||
|
sys.path.insert(0, os.path.join(os.getcwd(), "src"))
|
||||||
|
try:
|
||||||
|
from invisible_core.hooks import main
|
||||||
|
except ImportError as exc:
|
||||||
|
sys.exit("[pre-push] REFUSED - cannot import invisible_core.hooks (%s).\n"
|
||||||
|
"[pre-push] The pre-push policy lives in invisible-core. This repo is\n"
|
||||||
|
"[pre-push] either that package (so src/ is not where it should be) or\n"
|
||||||
|
"[pre-push] pins it exactly (so install it). Either way, nothing is\n"
|
||||||
|
"[pre-push] pushed past a hook that could not check it." % exc)
|
||||||
|
sys.exit(main())'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.venv/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
.env
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 feder-cr
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# dots
|
||||||
|
|
||||||
|
**Open-source dots for the web: an agent with its own browser, one that does not get blocked. Tell it what you want in plain language.**
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Windows, in PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||||
|
$env:Path = "$env:USERPROFILE\.local\bin;$env:Path"
|
||||||
|
uvx --from git+https://github.com/feder-cr/dots dots --openrouter-key sk-or-...
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
source $HOME/.local/bin/env
|
||||||
|
uvx --from git+https://github.com/feder-cr/dots dots --openrouter-key sk-or-...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open **http://127.0.0.1:8765**. Chat on the left, the live browser on the right.
|
||||||
|
|
||||||
|
## What to ask it
|
||||||
|
|
||||||
|
> Go to `<paste the URL>`. One way, Milan to Lisbon, economy, one adult. Check
|
||||||
|
> every date from the 12th to the 16th of next month and read the cheapest fare
|
||||||
|
> for each day. If a date has no availability, say so. Do not guess a number.
|
||||||
|
|
||||||
|
It drives the page the way a person would: the pointer moves, keys are pressed.
|
||||||
|
|
||||||
|
## From your own assistant
|
||||||
|
|
||||||
|
Claude Code, Codex, Gemini CLI or any MCP client: use
|
||||||
|
[invisible_playwright_mcp](https://github.com/feder-cr/invisible_playwright_mcp),
|
||||||
|
the same browser as a server. `dots` is its interface, and every option it takes
|
||||||
|
is listed by `dots --help`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Not affiliated with OpenAI. MIT licensed.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "dots"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Open-source dots for the web: an agent with its own browser, one that does not get blocked."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
license = "MIT"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
|
authors = [{ name = "feder-cr", email = "[email protected]" }]
|
||||||
|
keywords = ["dots", "browser-agent", "web-agent", "ai-agent", "anti-detect", "firefox", "openrouter"]
|
||||||
|
classifiers = [
|
||||||
|
"Operating System :: Microsoft :: Windows",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
# ⛔ THE WHOLE PRODUCT IS THIS LINE. `dots` is invisible-playwright-mcp's
|
||||||
|
# interface under another name: the loop, the browser and the page all
|
||||||
|
# come from there, and nothing here re-implements any of it. An EXACT pin
|
||||||
|
# and not a floor, because `cli.py` calls into that package's command
|
||||||
|
# group, which is not a published API: a new release is taken on purpose,
|
||||||
|
# with `tests/test_entry.py` run against it, never picked up by a resolver.
|
||||||
|
"invisible-playwright-mcp==0.70.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/feder-cr/dots"
|
||||||
|
Repository = "https://github.com/feder-cr/dots"
|
||||||
|
"Use it from your assistant" = "https://github.com/feder-cr/invisible_playwright_mcp"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
dots = "dots.cli:main"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = ["pytest>=7"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
markers = [
|
||||||
|
"unit: pure logic - no I/O, no network, no subprocess, no browser",
|
||||||
|
"integration: runs the real command in a subprocess - no network, no browser",
|
||||||
|
]
|
||||||
|
addopts = "--strict-markers"
|
||||||
|
pythonpath = ["src"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/dots"]
|
||||||
|
|
||||||
|
# Which pre-push gates this repository wants: `.githooks/pre-push` hands over
|
||||||
|
# to invisible_core.hooks, which arrives here through the pinned package.
|
||||||
|
# `pin` is off because this repository pins invisible-playwright-mcp, not the
|
||||||
|
# core, and there are no release tags because nothing is published to an index:
|
||||||
|
# the name `dots` on PyPI belongs to an unrelated project.
|
||||||
|
[tool.invisible.hooks]
|
||||||
|
pytest = true
|
||||||
|
pin = false
|
||||||
|
english = true
|
||||||
|
identity = true
|
||||||
|
release_tags = []
|
||||||
|
|
||||||
|
[tool.invisible.english]
|
||||||
|
excluded = []
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""dots: invisible-playwright-mcp's interface, under the name people search for."""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .cli import main
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""`dots` is `invisible-playwright-mcp ui`, and nothing else.
|
||||||
|
|
||||||
|
It goes through the package's command GROUP rather than calling the `ui`
|
||||||
|
command directly, because the group is where the `.env` beside the command is
|
||||||
|
read and where an old session directory is carried over. Calling `ui` on its
|
||||||
|
own would skip both, and a key kept in `.env` would simply not be found.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
#: The one subcommand this program is. Every option after it is the
|
||||||
|
#: interface's own, so `dots --help` lists exactly what `ui --help` does.
|
||||||
|
SUBCOMMAND = "ui"
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> None:
|
||||||
|
from invisible_playwright_mcp.cli import main as group
|
||||||
|
|
||||||
|
args = list(sys.argv[1:] if argv is None else argv)
|
||||||
|
group(args=[SUBCOMMAND, *args], prog_name="dots")
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""`dots` is the interface of the pinned invisible-playwright-mcp, and only that.
|
||||||
|
|
||||||
|
These tests are what a bump of the pin is judged by: the package's command
|
||||||
|
group is not a published API, so the day it changes shape is the day one of
|
||||||
|
these goes red.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.metadata
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import dots.cli
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _run(*args: str, cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, "-m", "dots", *args],
|
||||||
|
cwd=cwd, env=env, capture_output=True, text=True, timeout=120,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_env(home: Path) -> dict:
|
||||||
|
"""This environment without a model key, and with its own data directory."""
|
||||||
|
env = {k: v for k, v in os.environ.items()
|
||||||
|
if k.upper() not in {"OPENROUTER_API_KEY", "OPENAI_API_KEY"}}
|
||||||
|
env["INVISIBLE_MCP_HOME"] = str(home)
|
||||||
|
env["PYTHONPATH"] = str(ROOT / "src")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_the_group_is_called_with_ui_in_front(monkeypatch):
|
||||||
|
# Through the GROUP, because the group is what reads `.env`. A `dots` that
|
||||||
|
# called the `ui` command on its own would pass every other test here and
|
||||||
|
# never find a key kept in a file.
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
def group(*, args, prog_name):
|
||||||
|
seen["args"], seen["prog_name"] = args, prog_name
|
||||||
|
|
||||||
|
monkeypatch.setattr("invisible_playwright_mcp.cli.main", group)
|
||||||
|
dots.cli.main(["--port", "9000", "--headed"])
|
||||||
|
assert seen == {"args": ["ui", "--port", "9000", "--headed"], "prog_name": "dots"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_help_is_the_interface_help(tmp_path):
|
||||||
|
done = _run("--help", cwd=tmp_path, env=_clean_env(tmp_path / "home"))
|
||||||
|
assert done.returncode == 0, done.stderr
|
||||||
|
assert "Usage: dots" in done.stdout
|
||||||
|
for option in ("--openrouter-key", "--model", "--proxy", "--seed", "--profile-dir"):
|
||||||
|
assert option in done.stdout
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_without_a_key_it_refuses_before_starting_anything(tmp_path):
|
||||||
|
done = _run(cwd=tmp_path, env=_clean_env(tmp_path / "home"))
|
||||||
|
assert done.returncode != 0
|
||||||
|
assert "OpenRouter" in done.stdout + done.stderr
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_the_installed_package_is_the_pinned_one():
|
||||||
|
# An editable install of a sibling checkout would make every test above
|
||||||
|
# judge code that `uvx` never delivers.
|
||||||
|
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
|
||||||
|
(line,) = [d for d in pyproject["project"]["dependencies"]
|
||||||
|
if d.startswith("invisible-playwright-mcp")]
|
||||||
|
pinned = re.fullmatch(r"invisible-playwright-mcp==(\S+)", line).group(1)
|
||||||
|
assert importlib.metadata.version("invisible-playwright-mcp") == pinned
|
||||||
Reference in new issue
Block a user