gemx 0.1.0__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.
gemx-0.1.0/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # uv / venv
10
+ .venv/
11
+ venv/
12
+ uv.lock
13
+
14
+ # Tooling caches
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .pytest_cache/
18
+
19
+ # Playwright profile data (never commit a logged-in session)
20
+ *.jimi-profile/
21
+ profile/
22
+
23
+ # OS
24
+ .DS_Store
gemx-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ra0x3 / Stonehedge Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gemx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: gemx
3
+ Version: 0.1.0
4
+ Summary: Drive the Gemini web UI from Python via Playwright
5
+ Project-URL: Homepage, https://github.com/ra0x3/gemx
6
+ Project-URL: Repository, https://github.com/ra0x3/gemx
7
+ Project-URL: Issues, https://github.com/ra0x3/gemx/issues
8
+ Author: ra0x3
9
+ Maintainer: ra0x3
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: automation,browser,gemini,llm,playwright,scraping
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: playwright>=1.49
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Gemx
24
+
25
+ **Drive the Gemini web UI from Python.** A play on "Gemini" — Gemx treats
26
+ `gemini.google.com` as if it were an API, using Playwright to enter a prompt,
27
+ submit it, and capture the structured reply.
28
+
29
+ It exists because the obvious approaches don't work: Gemini's editor is
30
+ [Quill](https://quilljs.com/), which keeps its own document model and ignores
31
+ DOM surgery, Playwright `fill()`, and synthetic input events — those leave the
32
+ model empty and the turn errors with *"I encountered an error."* Gemx injects
33
+ text via `execCommand('insertText')` (the same trusted input pipeline a manual
34
+ paste uses), reads the reply from `message-content .markdown`, and retains the
35
+ *peak* streamed text because Gemini re-mounts the response node mid-stream.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ uv add gemx
41
+ # Playwright needs a browser the first time:
42
+ uv run playwright install chromium
43
+ ```
44
+
45
+ ## CLI
46
+
47
+ ```bash
48
+ # JSON (default)
49
+ gemx "List 3 NBA teams as a JSON array"
50
+
51
+ # XML
52
+ gemx --format xml "Describe the solar system as XML"
53
+
54
+ # Plain text, reading the prompt from stdin
55
+ echo "Summarize the plot of Dune in one sentence" | gemx --format txt
56
+
57
+ # Watch the browser while it works
58
+ gemx --headful --verbose "Hello there"
59
+ ```
60
+
61
+ The chosen `--format` (`json`, `xml`, or `txt`) is appended to the prompt as an
62
+ instruction *and* drives how Gemx parses the reply.
63
+
64
+ | Option | Description |
65
+ | --- | --- |
66
+ | `-f, --format {json,xml,txt}` | Output format (default: `json`). |
67
+ | `-p, --profile-dir PATH` | Chrome profile dir (default: `~/.gemx/profile`). |
68
+ | `--headful` | Show the browser window. |
69
+ | `--response-timeout SECONDS` | Wait for a response to start (default: 180). |
70
+ | `-v, --verbose` | Log progress to stderr. |
71
+
72
+ ## Library
73
+
74
+ ```python
75
+ import asyncio
76
+ from pathlib import Path
77
+ from gemx import Gemx, GemxConfig, OutputFormat
78
+
79
+
80
+ async def main() -> None:
81
+ config = GemxConfig(profile_dir=Path("~/.gemx/profile"))
82
+ async with Gemx(config) as gemx:
83
+ data = await gemx.ask("List 3 fruits as JSON", OutputFormat.JSON)
84
+ print(data)
85
+
86
+
87
+ asyncio.run(main())
88
+ ```
89
+
90
+ ## Authentication
91
+
92
+ Gemx drives a real, signed-in Gemini session. Point `--profile-dir` at a Chrome
93
+ profile that is already logged into your Google account (run once with
94
+ `--headful` to sign in); subsequent runs reuse that profile.
95
+
96
+ ## Development
97
+
98
+ ```bash
99
+ uv sync
100
+ uv run ruff check .
101
+ uv run mypy
102
+ uv run pylint src/gemx
103
+ uv run pytest
104
+ ```
105
+
106
+ The browser-console debugging scripts used to discover and verify the current
107
+ Gemini selectors live in [`tests/scripts/`](tests/scripts/).
108
+
109
+ ## License
110
+
111
+ MIT © ra0x3 — [Stonehedge Labs](https://github.com/stonehedgelabs)
gemx-0.1.0/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Gemx
2
+
3
+ **Drive the Gemini web UI from Python.** A play on "Gemini" — Gemx treats
4
+ `gemini.google.com` as if it were an API, using Playwright to enter a prompt,
5
+ submit it, and capture the structured reply.
6
+
7
+ It exists because the obvious approaches don't work: Gemini's editor is
8
+ [Quill](https://quilljs.com/), which keeps its own document model and ignores
9
+ DOM surgery, Playwright `fill()`, and synthetic input events — those leave the
10
+ model empty and the turn errors with *"I encountered an error."* Gemx injects
11
+ text via `execCommand('insertText')` (the same trusted input pipeline a manual
12
+ paste uses), reads the reply from `message-content .markdown`, and retains the
13
+ *peak* streamed text because Gemini re-mounts the response node mid-stream.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ uv add gemx
19
+ # Playwright needs a browser the first time:
20
+ uv run playwright install chromium
21
+ ```
22
+
23
+ ## CLI
24
+
25
+ ```bash
26
+ # JSON (default)
27
+ gemx "List 3 NBA teams as a JSON array"
28
+
29
+ # XML
30
+ gemx --format xml "Describe the solar system as XML"
31
+
32
+ # Plain text, reading the prompt from stdin
33
+ echo "Summarize the plot of Dune in one sentence" | gemx --format txt
34
+
35
+ # Watch the browser while it works
36
+ gemx --headful --verbose "Hello there"
37
+ ```
38
+
39
+ The chosen `--format` (`json`, `xml`, or `txt`) is appended to the prompt as an
40
+ instruction *and* drives how Gemx parses the reply.
41
+
42
+ | Option | Description |
43
+ | --- | --- |
44
+ | `-f, --format {json,xml,txt}` | Output format (default: `json`). |
45
+ | `-p, --profile-dir PATH` | Chrome profile dir (default: `~/.gemx/profile`). |
46
+ | `--headful` | Show the browser window. |
47
+ | `--response-timeout SECONDS` | Wait for a response to start (default: 180). |
48
+ | `-v, --verbose` | Log progress to stderr. |
49
+
50
+ ## Library
51
+
52
+ ```python
53
+ import asyncio
54
+ from pathlib import Path
55
+ from gemx import Gemx, GemxConfig, OutputFormat
56
+
57
+
58
+ async def main() -> None:
59
+ config = GemxConfig(profile_dir=Path("~/.gemx/profile"))
60
+ async with Gemx(config) as gemx:
61
+ data = await gemx.ask("List 3 fruits as JSON", OutputFormat.JSON)
62
+ print(data)
63
+
64
+
65
+ asyncio.run(main())
66
+ ```
67
+
68
+ ## Authentication
69
+
70
+ Gemx drives a real, signed-in Gemini session. Point `--profile-dir` at a Chrome
71
+ profile that is already logged into your Google account (run once with
72
+ `--headful` to sign in); subsequent runs reuse that profile.
73
+
74
+ ## Development
75
+
76
+ ```bash
77
+ uv sync
78
+ uv run ruff check .
79
+ uv run mypy
80
+ uv run pylint src/gemx
81
+ uv run pytest
82
+ ```
83
+
84
+ The browser-console debugging scripts used to discover and verify the current
85
+ Gemini selectors live in [`tests/scripts/`](tests/scripts/).
86
+
87
+ ## License
88
+
89
+ MIT © ra0x3 — [Stonehedge Labs](https://github.com/stonehedgelabs)
@@ -0,0 +1,94 @@
1
+ [project]
2
+ name = "gemx"
3
+ version = "0.1.0"
4
+ description = "Drive the Gemini web UI from Python via Playwright"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "ra0x3" }]
9
+ maintainers = [{ name = "ra0x3" }]
10
+ keywords = ["gemini", "playwright", "automation", "llm", "browser", "scraping"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Topic :: Software Development :: Libraries :: Python Modules",
17
+ "Typing :: Typed",
18
+ ]
19
+ dependencies = [
20
+ "playwright>=1.49",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/ra0x3/gemx"
25
+ Repository = "https://github.com/ra0x3/gemx"
26
+ Issues = "https://github.com/ra0x3/gemx/issues"
27
+
28
+ [project.scripts]
29
+ gemx = "gemx.cli:main"
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "mypy>=1.13",
34
+ "ruff>=0.8",
35
+ "pylint>=3.3",
36
+ "pytest>=8.3",
37
+ "pytest-asyncio>=0.24",
38
+ ]
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/gemx"]
46
+
47
+ [tool.ruff]
48
+ line-length = 88
49
+ target-version = "py312"
50
+ src = ["src", "tests"]
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "SIM", "RUF"]
54
+ ignore = ["A003"]
55
+
56
+ [tool.ruff.lint.isort]
57
+ known-first-party = ["gemx"]
58
+
59
+ [tool.mypy]
60
+ python_version = "3.12"
61
+ strict = true
62
+ warn_unused_configs = true
63
+ warn_redundant_casts = true
64
+ warn_unused_ignores = true
65
+ disallow_any_generics = true
66
+ disallow_untyped_defs = true
67
+ no_implicit_optional = true
68
+ files = ["src/gemx"]
69
+
70
+ [[tool.mypy.overrides]]
71
+ module = "tests.*"
72
+ disallow_untyped_defs = false
73
+
74
+ [[tool.mypy.overrides]]
75
+ module = "playwright.*"
76
+ ignore_missing_imports = true
77
+
78
+ [tool.pylint.main]
79
+ py-version = "3.12"
80
+ source-roots = ["src"]
81
+
82
+ [tool.pylint.format]
83
+ max-line-length = 88
84
+
85
+ [tool.pylint."messages control"]
86
+ disable = [
87
+ "missing-module-docstring",
88
+ "too-few-public-methods",
89
+ "duplicate-code",
90
+ ]
91
+
92
+ [tool.pytest.ini_options]
93
+ asyncio_mode = "auto"
94
+ testpaths = ["tests"]
@@ -0,0 +1,29 @@
1
+ """Gemx — drive the Gemini web UI from Python.
2
+
3
+ A play on "Gemini". Treats ``gemini.google.com`` as if it were an API.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .client import Gemx, GemxConfig
9
+ from .errors import (
10
+ GemxError,
11
+ InputError,
12
+ ResponseParseError,
13
+ ResponseTimeoutError,
14
+ )
15
+ from .formats import OutputFormat, format_instruction, parse_output
16
+
17
+ __all__ = [
18
+ "Gemx",
19
+ "GemxConfig",
20
+ "GemxError",
21
+ "InputError",
22
+ "OutputFormat",
23
+ "ResponseParseError",
24
+ "ResponseTimeoutError",
25
+ "format_instruction",
26
+ "parse_output",
27
+ ]
28
+
29
+ __version__ = "0.1.0"
@@ -0,0 +1,114 @@
1
+ """Command-line interface: ``gemx``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import json
8
+ import logging
9
+ import sys
10
+ from pathlib import Path
11
+ from xml.etree import ElementTree as ET
12
+
13
+ from . import __version__
14
+ from .client import Gemx, GemxConfig
15
+ from .errors import GemxError
16
+ from .formats import OutputFormat
17
+
18
+ DEFAULT_PROFILE_DIR = Path("~/.gemx/profile")
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ """Construct the ``gemx`` argument parser."""
23
+ parser = argparse.ArgumentParser(
24
+ prog="gemx",
25
+ description="Drive the Gemini web UI from the command line.",
26
+ )
27
+ parser.add_argument("--version", action="version", version=f"gemx {__version__}")
28
+ parser.add_argument(
29
+ "prompt",
30
+ nargs="?",
31
+ help="Prompt to send. If omitted, read from stdin.",
32
+ )
33
+ parser.add_argument(
34
+ "-f",
35
+ "--format",
36
+ type=OutputFormat.from_str,
37
+ default=OutputFormat.JSON,
38
+ metavar="{json,xml,txt}",
39
+ help="Output format requested from Gemini (default: json).",
40
+ )
41
+ parser.add_argument(
42
+ "-p",
43
+ "--profile-dir",
44
+ type=Path,
45
+ default=DEFAULT_PROFILE_DIR,
46
+ help=f"Chrome profile dir for the session (default: {DEFAULT_PROFILE_DIR}).",
47
+ )
48
+ parser.add_argument(
49
+ "--headful",
50
+ action="store_true",
51
+ help="Show the browser window instead of running headless.",
52
+ )
53
+ parser.add_argument(
54
+ "--response-timeout",
55
+ type=int,
56
+ default=180,
57
+ metavar="SECONDS",
58
+ help="Max seconds to wait for a response to start (default: 180).",
59
+ )
60
+ parser.add_argument(
61
+ "-v",
62
+ "--verbose",
63
+ action="store_true",
64
+ help="Log progress to stderr.",
65
+ )
66
+ return parser
67
+
68
+
69
+ def _render(value: object, fmt: OutputFormat) -> str:
70
+ """Serialize a parsed value back to a string for stdout."""
71
+ if fmt is OutputFormat.JSON:
72
+ return json.dumps(value, indent=2, ensure_ascii=False)
73
+ if fmt is OutputFormat.XML:
74
+ assert isinstance(value, ET.Element)
75
+ return ET.tostring(value, encoding="unicode")
76
+ return str(value)
77
+
78
+
79
+ async def _run(args: argparse.Namespace) -> int:
80
+ prompt = args.prompt if args.prompt is not None else sys.stdin.read()
81
+ prompt = prompt.strip()
82
+ if not prompt:
83
+ print("error: empty prompt", file=sys.stderr)
84
+ return 2
85
+
86
+ config = GemxConfig(
87
+ profile_dir=args.profile_dir,
88
+ headless=not args.headful,
89
+ response_timeout_s=args.response_timeout,
90
+ )
91
+ try:
92
+ async with Gemx(config) as gemx:
93
+ result = await gemx.ask(prompt, args.format)
94
+ except GemxError as exc:
95
+ print(f"error: {exc}", file=sys.stderr)
96
+ return 1
97
+
98
+ print(_render(result, args.format))
99
+ return 0
100
+
101
+
102
+ def main(argv: list[str] | None = None) -> int:
103
+ """Entry point for the ``gemx`` console script."""
104
+ args = build_parser().parse_args(argv)
105
+ logging.basicConfig(
106
+ level=logging.INFO if args.verbose else logging.WARNING,
107
+ format="%(levelname)s %(name)s: %(message)s",
108
+ stream=sys.stderr,
109
+ )
110
+ return asyncio.run(_run(args))
111
+
112
+
113
+ if __name__ == "__main__":
114
+ raise SystemExit(main())