taprun 0.14.0__py3-none-any.whl

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.
taprun/__init__.py ADDED
@@ -0,0 +1,59 @@
1
+ """Taprun — compile your AI agent's browser-acting trajectory into a
2
+ deterministic program that replays at zero LLM tokens.
3
+
4
+ Two surfaces in one package:
5
+
6
+ 1. **CLI**. After `pip install taprun` the `tap` command becomes
7
+ available; first invocation downloads the platform binary from npm
8
+ and exec's it. Equivalent to `brew install LeonTing1010/tap/tap` for
9
+ Python users on Linux/Windows where Homebrew isn't standard.
10
+
11
+ 2. **SDK**. `from taprun import forge, run, doctor` shells out to the
12
+ same `tap` CLI. Each call is one process spawn (~30-100 ms warm);
13
+ for hot loops use `tap mcp start` and call via MCP.
14
+
15
+ Quick start
16
+ -----------
17
+
18
+ >>> from taprun import run, doctor
19
+ >>> rows = run("hn/top") # execute a Tap-compiled plan
20
+ >>> verdict = doctor("hn/top") # cross-validate vs authoritative
21
+
22
+ >>> from browser_use import Agent
23
+ >>> from taprun import forge
24
+ >>> agent = Agent(task="...", llm=...)
25
+ >>> result = await agent.run()
26
+ >>> forge(trajectory=result.model_dump(),
27
+ ... site="example", name="dashboard")
28
+
29
+ See: https://taprun.dev/docs
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from .api import (
35
+ forge,
36
+ run,
37
+ doctor,
38
+ verify,
39
+ list_taps,
40
+ show,
41
+ stats,
42
+ TapError,
43
+ TapNotFoundError,
44
+ )
45
+
46
+ __version__ = "0.14.0"
47
+
48
+ __all__ = [
49
+ "forge",
50
+ "run",
51
+ "doctor",
52
+ "verify",
53
+ "list_taps",
54
+ "show",
55
+ "stats",
56
+ "TapError",
57
+ "TapNotFoundError",
58
+ "__version__",
59
+ ]
taprun/api.py ADDED
@@ -0,0 +1,222 @@
1
+ """Subprocess wrapper around the `tap` CLI.
2
+
3
+ Each public function shells out to the binary located via
4
+ `TAPRUN_BIN` env var or `tap` on PATH. Output is parsed as JSON
5
+ where the CLI supports `--json`; raw stdout otherwise.
6
+
7
+ Errors:
8
+ - `TapNotFoundError` when the `tap` binary cannot be located.
9
+ - `TapError` (non-zero exit) carrying stderr + the failed argv.
10
+
11
+ Latency note: each call is one process spawn (~30-100 ms on a
12
+ warm machine). Acceptable for batch agent jobs; for hot loops
13
+ prefer `tap mcp start` and call via MCP.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import shutil
21
+ import subprocess
22
+ import tempfile
23
+ from typing import Any, Optional, Union
24
+
25
+
26
+ class TapError(RuntimeError):
27
+ """Raised when the `tap` CLI exits non-zero."""
28
+
29
+ def __init__(self, argv: list[str], returncode: int, stderr: str) -> None:
30
+ self.argv = argv
31
+ self.returncode = returncode
32
+ self.stderr = stderr
33
+ super().__init__(
34
+ f"tap {' '.join(argv[1:])} exited {returncode}: {stderr.strip() or '(no stderr)'}"
35
+ )
36
+
37
+
38
+ class TapNotFoundError(RuntimeError):
39
+ """Raised when the `tap` binary cannot be located. Install via brew
40
+ or set TAPRUN_BIN to the path."""
41
+
42
+
43
+ def _resolve_bin() -> str:
44
+ explicit = os.environ.get("TAPRUN_BIN")
45
+ if explicit:
46
+ if not os.path.isfile(explicit):
47
+ raise TapNotFoundError(f"TAPRUN_BIN={explicit} does not exist")
48
+ return explicit
49
+ found = shutil.which("tap")
50
+ if not found:
51
+ raise TapNotFoundError(
52
+ "`tap` not found on PATH. Install with `brew install LeonTing1010/tap/tap` "
53
+ "or set TAPRUN_BIN to the binary path."
54
+ )
55
+ return found
56
+
57
+
58
+ def _run_cli(args: list[str], *, parse_json: bool = True) -> Any:
59
+ """Spawn the `tap` CLI with the given args. Returns parsed JSON
60
+ when `parse_json=True` and the output is parsable, else raw text.
61
+ Raises `TapError` on non-zero exit."""
62
+ bin_path = _resolve_bin()
63
+ argv = [bin_path] + args
64
+ proc = subprocess.run(
65
+ argv,
66
+ capture_output=True,
67
+ text=True,
68
+ check=False,
69
+ )
70
+ if proc.returncode != 0:
71
+ raise TapError(argv, proc.returncode, proc.stderr)
72
+ out = proc.stdout
73
+ if not parse_json:
74
+ return out
75
+ try:
76
+ return json.loads(out)
77
+ except json.JSONDecodeError:
78
+ # Some CLI commands print a mix of progress text + a JSON tail.
79
+ # Best-effort: return raw text and let the caller parse.
80
+ return out
81
+
82
+
83
+ def run(tap_id: str, *, args: Optional[dict[str, Any]] = None) -> Any:
84
+ """Execute a compiled tap by `<site>/<name>` id. Returns parsed
85
+ rows (typically a list of dicts).
86
+
87
+ >>> rows = run("hn/top")
88
+ >>> len(rows) > 0
89
+ True
90
+ """
91
+ cli_args = ["run", tap_id, "--json"]
92
+ if args:
93
+ for k, v in args.items():
94
+ cli_args.extend([f"--{k}", str(v)])
95
+ return _run_cli(cli_args)
96
+
97
+
98
+ def doctor(tap_id: str) -> Any:
99
+ """Run health + drift verification on a tap. Returns the doctor
100
+ verdict (status + details).
101
+
102
+ >>> v = doctor("hn/top")
103
+ >>> v["status"] in ("ok", "broken", "stale")
104
+ True
105
+ """
106
+ return _run_cli(["doctor", tap_id, "--json"])
107
+
108
+
109
+ # `verify` is an alias for `doctor` — both names appear in the
110
+ # three-plane refactor ADR. SDK exposes both for ergonomic choice.
111
+ verify = doctor
112
+
113
+
114
+ def forge(
115
+ target: Optional[str] = None,
116
+ *,
117
+ desc: Optional[str] = None,
118
+ intent: str = "read",
119
+ trajectory: Union[str, dict, list, None] = None,
120
+ site: Optional[str] = None,
121
+ name: Optional[str] = None,
122
+ ) -> Any:
123
+ """Compile a tap from a URL, natural-language description, OR a
124
+ browser-use AgentHistoryList trajectory.
125
+
126
+ Three modes:
127
+
128
+ 1. URL — Tier 0 deterministic compile, no AI:
129
+
130
+ >>> forge("https://news.ycombinator.com")
131
+
132
+ 2. Description (BYOK AI loop):
133
+
134
+ >>> forge("get github trending repos")
135
+
136
+ 3. Trajectory — compile a recorded agent run into a deterministic
137
+ plan that replays at zero LLM tokens:
138
+
139
+ >>> from browser_use import Agent
140
+ >>> agent = Agent(task="...", llm=...)
141
+ >>> result = await agent.run()
142
+ >>> forge(trajectory=result.model_dump(),
143
+ ... site="ycombinator", name="homepage")
144
+
145
+ Pass either a parsed dict / AgentHistoryList JSON, or a path
146
+ to a JSON file. `site` and `name` are required.
147
+ """
148
+ if trajectory is not None:
149
+ if not site or not name:
150
+ raise ValueError("forge(trajectory=...) requires both site= and name=")
151
+ if isinstance(trajectory, str) and os.path.isfile(trajectory):
152
+ traj_path = trajectory
153
+ cleanup_path: Optional[str] = None
154
+ else:
155
+ payload = trajectory if isinstance(trajectory, str) else json.dumps(trajectory)
156
+ fd, traj_path = tempfile.mkstemp(suffix=".trajectory.json")
157
+ try:
158
+ with os.fdopen(fd, "w") as f:
159
+ f.write(payload)
160
+ except BaseException:
161
+ os.unlink(traj_path)
162
+ raise
163
+ cleanup_path = traj_path
164
+ try:
165
+ return _run_cli(
166
+ ["forge", "--from-trajectory", traj_path, "--site", site, "--name", name],
167
+ parse_json=False,
168
+ )
169
+ finally:
170
+ if cleanup_path:
171
+ try:
172
+ os.unlink(cleanup_path)
173
+ except OSError:
174
+ pass
175
+
176
+ if target is None:
177
+ raise ValueError("forge() requires `target` (URL or description) or `trajectory=`")
178
+ args = ["forge", target]
179
+ if desc:
180
+ args.extend(["--desc", desc])
181
+ if intent != "read":
182
+ args.extend(["--intent", intent])
183
+ return _run_cli(args, parse_json=False)
184
+
185
+
186
+ def list_taps(site: Optional[str] = None) -> Any:
187
+ """List installed taps. Optional site filter.
188
+
189
+ >>> taps = list_taps("hn")
190
+ """
191
+ args = ["list"]
192
+ if site:
193
+ args.append(site)
194
+ args.append("--json")
195
+ return _run_cli(args)
196
+
197
+
198
+ def show(tap_id: str) -> Any:
199
+ """Inspect a tap (source + manifest + fingerprint).
200
+
201
+ >>> info = show("hn/top")
202
+ """
203
+ return _run_cli(["show", tap_id])
204
+
205
+
206
+ def stats() -> Any:
207
+ """K(t) telemetry — token cost per capability accumulated across
208
+ runs. Returns the stats snapshot in the same shape as
209
+ `tap stats --json`.
210
+
211
+ >>> s = stats()
212
+ """
213
+ return _run_cli(["stats", "--json"])
214
+
215
+
216
+ def _which() -> Union[str, None]:
217
+ """Diagnostic — return the `tap` binary path that would be used,
218
+ or None if not found. Public for users debugging install paths."""
219
+ try:
220
+ return _resolve_bin()
221
+ except TapNotFoundError:
222
+ return None
taprun/cli.py ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """Tap CLI - Python wrapper for the tap binary."""
3
+
4
+ import os
5
+ import sys
6
+ import platform
7
+ import subprocess
8
+ import urllib.request
9
+ import urllib.error
10
+ import json
11
+ from pathlib import Path
12
+
13
+ VERSION = "0.14.0"
14
+ GITHUB_REPO = "LeonTing1010/tap"
15
+ NPM_REGISTRY = "https://registry.npmjs.org/@taprun/cli"
16
+ CACHE_DIR = Path.home() / ".tap" / "bin"
17
+
18
+
19
+ def get_platform_package():
20
+ """Determine the correct npm package name for the current platform."""
21
+ system = platform.system().lower()
22
+ machine = platform.machine().lower()
23
+
24
+ if system == "darwin":
25
+ if machine in ("arm64", "aarch64"):
26
+ return "cli-darwin-arm64"
27
+ else:
28
+ return "cli-darwin-x64"
29
+ elif system == "linux":
30
+ return "cli-linux-x64"
31
+ elif system == "windows":
32
+ return "cli-win32-x64"
33
+ else:
34
+ raise RuntimeError(f"Unsupported platform: {system} {machine}")
35
+
36
+
37
+ def get_binary_name():
38
+ """Get the binary name for the current platform."""
39
+ system = platform.system().lower()
40
+ if system == "windows":
41
+ return "tap.exe"
42
+ return "tap"
43
+
44
+
45
+ def download_from_npm():
46
+ """Download the tap binary from npm registry."""
47
+ package = get_platform_package()
48
+ binary_name = get_binary_name()
49
+
50
+ # Get package metadata from npm
51
+ npm_url = f"https://registry.npmjs.org/@taprun/{package}"
52
+
53
+ try:
54
+ with urllib.request.urlopen(npm_url, timeout=30) as response:
55
+ data = json.loads(response.read().decode('utf-8'))
56
+
57
+ # Get the latest version's tarball URL
58
+ latest_version = data.get('dist-tags', {}).get('latest', VERSION)
59
+ version_data = data.get('versions', {}).get(latest_version, {})
60
+ tarball_url = version_data.get('dist', {}).get('tarball', '')
61
+
62
+ if not tarball_url:
63
+ raise RuntimeError(f"Could not find tarball URL for {package}@{latest_version}")
64
+
65
+ print(f"Downloading tap {latest_version} for your platform...")
66
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
67
+
68
+ # Download tarball
69
+ import tarfile
70
+ import tempfile
71
+
72
+ with tempfile.NamedTemporaryFile(suffix='.tgz', delete=False) as tmp:
73
+ urllib.request.urlretrieve(tarball_url, tmp.name)
74
+
75
+ # Extract the binary
76
+ with tarfile.open(tmp.name, 'r:gz') as tar:
77
+ # Find the binary in the package
78
+ for member in tar.getmembers():
79
+ if member.name.endswith(f'bin/{binary_name}') or member.name.endswith(binary_name):
80
+ # Extract to cache dir
81
+ extracted = tar.extract(member, CACHE_DIR)
82
+ # Get the actual file path
83
+ extracted_path = CACHE_DIR / member.name
84
+ final_path = CACHE_DIR / binary_name
85
+
86
+ # Move to final location if needed
87
+ if extracted_path != final_path:
88
+ extracted_path.rename(final_path)
89
+
90
+ # Make executable on Unix systems
91
+ if platform.system() != "Windows":
92
+ final_path.chmod(0o755)
93
+
94
+ # Cleanup temp files
95
+ os.unlink(tmp.name)
96
+ # Cleanup extracted directory structure
97
+ import shutil
98
+ pkg_dir = CACHE_DIR / "package"
99
+ if pkg_dir.exists():
100
+ shutil.rmtree(pkg_dir)
101
+
102
+ print(f"Downloaded to {final_path}")
103
+ return final_path
104
+
105
+ raise RuntimeError(f"Binary not found in package {package}")
106
+
107
+ except urllib.error.HTTPError as e:
108
+ raise RuntimeError(f"Failed to download tap binary: HTTP {e.code}")
109
+ except Exception as e:
110
+ raise RuntimeError(f"Failed to download tap binary: {e}")
111
+
112
+
113
+ def get_binary_path():
114
+ """Get the path to the tap binary, downloading if necessary.
115
+
116
+ Resolution order (matches taprun.api._resolve_bin):
117
+ 1. TAPRUN_BIN env var (explicit override; e.g. brew-installed tap)
118
+ 2. ~/.tap/bin/tap (cached download from npm)
119
+ 3. Download from @taprun/cli-* npm registry → ~/.tap/bin/tap
120
+ """
121
+ explicit = os.environ.get("TAPRUN_BIN")
122
+ if explicit:
123
+ explicit_path = Path(explicit)
124
+ if not explicit_path.is_file():
125
+ raise RuntimeError(f"TAPRUN_BIN={explicit} does not exist")
126
+ return explicit_path
127
+
128
+ binary_name = get_binary_name()
129
+ binary_path = CACHE_DIR / binary_name
130
+
131
+ # Check if binary already exists
132
+ if binary_path.exists():
133
+ return binary_path
134
+
135
+ # Download the binary from npm
136
+ return download_from_npm()
137
+
138
+
139
+ def main():
140
+ """Main entry point - proxy to the tap binary."""
141
+ try:
142
+ binary_path = get_binary_path()
143
+
144
+ # Pass all arguments to the tap binary
145
+ args = [str(binary_path)] + sys.argv[1:]
146
+
147
+ # Execute the binary
148
+ result = subprocess.run(args, check=False)
149
+ sys.exit(result.returncode)
150
+ except KeyboardInterrupt:
151
+ sys.exit(130)
152
+ except Exception as e:
153
+ print(f"Error: {e}", file=sys.stderr)
154
+ sys.exit(1)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ main()
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: taprun
3
+ Version: 0.14.0
4
+ Summary: Compile your AI agent's browser-acting trajectory into a deterministic program that replays at zero LLM tokens. CLI + Python SDK.
5
+ Author-email: Leon Ting <leonting1010@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://taprun.dev
8
+ Project-URL: Documentation, https://taprun.dev/docs
9
+ Project-URL: Repository, https://github.com/LeonTing1010/tap
10
+ Project-URL: Issues, https://github.com/LeonTing1010/tap/issues
11
+ Keywords: ai-agent,browser-automation,web-scraping,mcp,claude,cursor,browser-use,stagehand,deterministic,trajectory-compile
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+
27
+ # taprun
28
+
29
+ Compile your AI agent's browser-acting trajectory into a deterministic
30
+ program that replays at **zero LLM tokens**, with built-in drift
31
+ verification.
32
+
33
+ ```bash
34
+ pip install taprun
35
+ ```
36
+
37
+ After install you get **two surfaces in one package**:
38
+
39
+ ## 1 · CLI
40
+
41
+ The `tap` command. First invocation downloads the platform binary from
42
+ the npm `@taprun/cli-*` packages; subsequent calls exec the cached
43
+ binary. Equivalent to `brew install LeonTing1010/tap/tap` for Python
44
+ users on Linux/Windows where Homebrew isn't standard.
45
+
46
+ ```bash
47
+ tap forge https://news.ycombinator.com/ # compile a tap (Tier 0, no AI)
48
+ tap run hackernews/top # zero LLM tokens
49
+ tap doctor hackernews/top # cross-validate vs authoritative
50
+ tap mcp start # MCP server for Claude Code / Cursor / Cline
51
+ ```
52
+
53
+ ## 2 · Python SDK
54
+
55
+ `from taprun import forge, run, doctor`. Each call shells out to the
56
+ same `tap` CLI; ~30-100 ms warm spawn cost. For hot loops use
57
+ `tap mcp start` and call via MCP instead.
58
+
59
+ ```python
60
+ from taprun import run, doctor
61
+
62
+ rows = run("hackernews/top") # execute a compiled tap
63
+ print(rows[0]["title"])
64
+
65
+ verdict = doctor("hackernews/top") # 'ok' / 'broken' / 'stale'
66
+ ```
67
+
68
+ ### Compile from a browser-use trajectory
69
+
70
+ ```python
71
+ from browser_use import Agent
72
+ from taprun import forge
73
+
74
+ agent = Agent(task="...", llm=...)
75
+ result = await agent.run()
76
+
77
+ forge(
78
+ trajectory=result.model_dump(), # AgentHistoryList dict, JSON string, or path
79
+ site="example",
80
+ name="dashboard",
81
+ )
82
+ # writes ~/.tap/taps/example/dashboard.tap.json
83
+ # replay with: tap run example/dashboard
84
+ ```
85
+
86
+ v1 trajectory compile covers the navigation skeleton (`go_to_url`,
87
+ `click_element`, `input_text`, `scroll`, `wait`, `done`). Navigation
88
+ steps replay at 0 LLM tokens. For destination pages exposing a Tier 0
89
+ source (RSS / JSON-LD / agents.json / OpenAPI), pair with `tap forge
90
+ <url>` to compile the extraction half — that combination delivers
91
+ end-to-end zero-token replay. Stagehand and raw Anthropic tool-use
92
+ formats land in a follow-up.
93
+
94
+ ## How it works
95
+
96
+ 1. **Forge**: inspect the site → compile a deterministic `.tap.json`
97
+ plan (one-time cost; free for pages with a Layer 1 source).
98
+ 2. **Run**: replay the plan, no LLM in the loop.
99
+ 3. **Doctor**: independently fetch the authoritative source and diff.
100
+ Catches drift the agent would miss on a self-replay.
101
+ 4. **Heal**: cached patches replay at 0 tokens; LLM only invoked when
102
+ the patch cache misses (Pro tier).
103
+
104
+ ## Configuration
105
+
106
+ ```bash
107
+ # Override the binary path (e.g. to use a brew-installed `tap`)
108
+ export TAPRUN_BIN=/opt/homebrew/bin/tap
109
+
110
+ # AI key for the forge pipeline (BYOK; Hacker tier and above)
111
+ tap config set ai.key sk-ant-...
112
+ ```
113
+
114
+ ## Pricing
115
+
116
+ - **Free**: 65+ community taps, run, doctor, Tier 0 forge (the
117
+ deterministic compile path when Layer 1 is available).
118
+ - **Hacker ($9/mo, BYOK)**: full forge pipeline with Layer 4 AI fallback.
119
+ - **Pro ($29/mo)**: heal + refresh + scheduling. 100% local.
120
+
121
+ The MCP server / CLI / forge / doctor binary is closed-source. The
122
+ public Chrome extension runtime
123
+ (<https://github.com/LeonTing1010/tap>) is MIT.
124
+
125
+ ## Links
126
+
127
+ - Homepage: <https://taprun.dev/?utm_source=pypi&utm_medium=package&utm_campaign=taprun>
128
+ - Docs: <https://taprun.dev/docs?utm_source=pypi&utm_medium=package&utm_campaign=taprun-docs>
129
+ - Issues: <https://github.com/LeonTing1010/tap/issues>
130
+ - Skills: <https://github.com/LeonTing1010/tap-skills>
131
+
132
+ MIT License (the public extension; the `tap` binary distributed by this
133
+ package is proprietary).
@@ -0,0 +1,8 @@
1
+ taprun/__init__.py,sha256=ap_6LfA7g4JBvlQwlt9Zg1Oor7N0ouEVB2s0wMFXpyg,1444
2
+ taprun/api.py,sha256=x6hh2kpRDY829ma5QP61pRSoe4G_XV_tMq8hxNQOk30,6668
3
+ taprun/cli.py,sha256=PLqljEQCCdIcwPYEpsL8G7ykULyhYJ5yC_Lc0_Tc6Us,5415
4
+ taprun-0.14.0.dist-info/METADATA,sha256=mImzLZTS92-W-ZGSYnRWTPv-Z9VZ9g-mmVfvYO9rXBM,4859
5
+ taprun-0.14.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ taprun-0.14.0.dist-info/entry_points.txt,sha256=EDv1lC5SPd2gAd_fdKYjjciCN7dUDlF1Dk9DYdKDCN0,40
7
+ taprun-0.14.0.dist-info/top_level.txt,sha256=TSeKX7hcsBArtKs0llrN5B5sZ-BJ0rxAON2PHQhZGHU,7
8
+ taprun-0.14.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tap = taprun.cli:main
@@ -0,0 +1 @@
1
+ taprun