mirror of
https://github.com/feder-cr/Jobs_Applier_AI_Agent.git
synced 2026-11-09 17:11:47 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
454d159b4c | ||
|
|
6db0edc8e2 | ||
|
|
cbbe1bc9c2 |
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,119 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# `gate` is the ONLY context the branch protection requires: it depends on every
|
||||
# job, so its name never drifts when the matrix changes. Unlike its siblings it
|
||||
# accepts nothing but success, because no job here is ever skipped on purpose:
|
||||
# a skip would mean a job that did not run, which reads exactly like one that
|
||||
# passed.
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
english:
|
||||
name: the repository is English
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: install the pinned package, which carries the gate
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .
|
||||
- name: check the prose
|
||||
run: |
|
||||
python -m invisible_core.english --selftest
|
||||
python -m invisible_core.english
|
||||
|
||||
claims:
|
||||
name: the README promises nothing it cannot support
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: install the pinned package, which carries the gate
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .
|
||||
- name: check the claims
|
||||
run: |
|
||||
python -m invisible_core.claims --selftest
|
||||
python -m invisible_core.claims
|
||||
|
||||
unit:
|
||||
name: pytest (${{ matrix.os }}, py${{ matrix.python }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
# Every version `requires-python = ">=3.11"` promises and the pinned
|
||||
# package declares, on ubuntu; Windows on 3.12.
|
||||
python: ["3.11", "3.12", "3.13"]
|
||||
exclude:
|
||||
- { os: windows-latest, python: "3.11" }
|
||||
- { os: windows-latest, python: "3.13" }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: pip
|
||||
- name: install the package and its dev extras
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e ".[dev]"
|
||||
- name: run pytest
|
||||
run: |
|
||||
set -o pipefail # or tee would swallow pytest's exit code
|
||||
pytest tests/ -v --tb=short -rs | tee tests-report.txt
|
||||
# pytest's exit code cannot tell "everything passed" from "the selection
|
||||
# dropped everything". The suite is four tests, so all four must run.
|
||||
- name: refuse a run in which the suite deselected itself
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import pathlib, re, sys
|
||||
text = pathlib.Path("tests-report.txt").read_text(encoding="utf-8", errors="ignore")
|
||||
m = re.search(r"(\d+) passed", text)
|
||||
passed = int(m.group(1)) if m else 0
|
||||
if passed < 4:
|
||||
print(f"only {passed} test(s) ran (expected 4)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f"{passed} tests ran")
|
||||
PY
|
||||
|
||||
gate:
|
||||
needs: [unit, english, claims]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: refuse unless every job succeeded
|
||||
run: |
|
||||
echo "unit: ${{ needs.unit.result }}"
|
||||
echo "english: ${{ needs.english.result }}"
|
||||
echo "claims: ${{ needs.claims.result }}"
|
||||
for result in "${{ needs.unit.result }}" "${{ needs.english.result }}" "${{ needs.claims.result }}"; do
|
||||
if [ "$result" != "success" ]; then
|
||||
echo "a job did not succeed: $result"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "ok"
|
||||
@@ -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.
|
||||
@@ -1,7 +1,67 @@
|
||||
# badges
|
||||
<div align="center">
|
||||
|
||||
Written every night by .github/workflows/counters.yml on main: the applications AIHawk has sent, for all its users together.
|
||||
# dots
|
||||
|
||||
- counters/history.csv: every counter, once a day
|
||||
- counters/applications_daily.csv: the applications of each measured day
|
||||
- docs/badges/applications.svg: the README badge
|
||||
**Every AI agent is a model and a browser.<br>You can swap the model with one flag. The browser is what the website sees.**
|
||||
|
||||
</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**. The conversation on the left, the browser on the right, live.
|
||||
|
||||
## It all comes down to the browser
|
||||
|
||||
When a web agent fails, the model is rarely why. The page never loaded, a
|
||||
challenge appeared, the login expired, the click did not land. All of that
|
||||
happens in the browser, before the model gets to think.
|
||||
|
||||
So dots is built around one:
|
||||
|
||||
- **A real Firefox engine, patched in C++.** The fingerprint is decided inside
|
||||
the engine, not painted over with JavaScript that a page can inspect.
|
||||
- **One identity per seed.** Screen, fonts, GPU, timezone and language agree
|
||||
with each other, and `--seed` gives back the same person on every run.
|
||||
- **Nothing for a page to find.** No WebDriver flag, no DevTools protocol, no
|
||||
automation globals in the page.
|
||||
- **A person's hands.** The pointer travels to what it clicks and keys are
|
||||
pressed one at a time, so every event the page receives is a trusted one.
|
||||
- **A browser that remembers.** `--profile-dir` keeps logins and cookies from
|
||||
one run to the next.
|
||||
- **Where it connects from is who it is.** With `--proxy`, the timezone and the
|
||||
language follow the exit.
|
||||
|
||||
The model is any model on OpenRouter, and `--model` changes it.
|
||||
|
||||
## 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.
|
||||
|
||||
## The same browser, in your own assistant
|
||||
|
||||
Claude Code, Codex, Gemini CLI or any MCP client:
|
||||
[invisible_playwright_mcp](https://github.com/feder-cr/invisible_playwright_mcp)
|
||||
gives them this browser as a server. `dots` is its interface, and `dots --help`
|
||||
lists every option.
|
||||
|
||||
---
|
||||
|
||||
Not affiliated with OpenAI. MIT licensed.
|
||||
@@ -1,2 +0,0 @@
|
||||
day_utc,sent,submitted,unconfirmed,refused,blocked,failed
|
||||
2026-11-09,3,1,2,3,6,4
|
||||
|
@@ -1,17 +0,0 @@
|
||||
date,key,value
|
||||
2026-11-09,applications,0
|
||||
2026-11-09,blocked,0
|
||||
2026-11-09,failed,0
|
||||
2026-11-09,refused,0
|
||||
2026-11-09,sent,0
|
||||
2026-11-09,stars,32419
|
||||
2026-11-09,submitted,0
|
||||
2026-11-09,unconfirmed,0
|
||||
2026-11-10,applications,16
|
||||
2026-11-10,blocked,6
|
||||
2026-11-10,failed,4
|
||||
2026-11-10,refused,3
|
||||
2026-11-10,sent,3
|
||||
2026-11-10,stars,32430
|
||||
2026-11-10,submitted,1
|
||||
2026-11-10,unconfirmed,2
|
||||
|
@@ -1,16 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="126" height="20" role="img" aria-label="applications sent: 3">
|
||||
<title>applications sent: 3</title>
|
||||
<linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>
|
||||
<clipPath id="r"><rect width="126" height="20" rx="3" fill="#fff"/></clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="113" height="20" fill="#555"/>
|
||||
<rect x="113" width="13" height="20" fill="#2ea44f"/>
|
||||
<rect width="126" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
||||
<text x="56.5" y="15" fill="#010101" fill-opacity=".3">applications sent</text>
|
||||
<text x="56.5" y="14">applications sent</text>
|
||||
<text x="119.5" y="15" fill="#010101" fill-opacity=".3">3</text>
|
||||
<text x="119.5" y="14">3</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 886 B |
@@ -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