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 +59 -0
- taprun/api.py +222 -0
- taprun/cli.py +158 -0
- taprun-0.14.0.dist-info/METADATA +133 -0
- taprun-0.14.0.dist-info/RECORD +8 -0
- taprun-0.14.0.dist-info/WHEEL +5 -0
- taprun-0.14.0.dist-info/entry_points.txt +2 -0
- taprun-0.14.0.dist-info/top_level.txt +1 -0
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 @@
|
|
|
1
|
+
taprun
|