sleight 0.1.0a1__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.
- sleight-0.1.0a1/.gitignore +32 -0
- sleight-0.1.0a1/LICENSE +21 -0
- sleight-0.1.0a1/PKG-INFO +158 -0
- sleight-0.1.0a1/README.md +129 -0
- sleight-0.1.0a1/docs//350/257/246/347/273/206/346/226/207/346/241/243/346/211/213/345/206/214.md +1774 -0
- sleight-0.1.0a1/pyproject.toml +62 -0
- sleight-0.1.0a1/sleight/__init__.py +138 -0
- sleight-0.1.0a1/sleight/_logging.py +53 -0
- sleight-0.1.0a1/sleight/core/__init__.py +1 -0
- sleight-0.1.0a1/sleight/core/_http.py +169 -0
- sleight-0.1.0a1/sleight/core/_redact.py +44 -0
- sleight-0.1.0a1/sleight/core/element.py +219 -0
- sleight-0.1.0a1/sleight/core/errors.py +102 -0
- sleight-0.1.0a1/sleight/core/human/__init__.py +64 -0
- sleight-0.1.0a1/sleight/core/human/curves.py +297 -0
- sleight-0.1.0a1/sleight/core/human/engine.py +435 -0
- sleight-0.1.0a1/sleight/core/human/presets.py +160 -0
- sleight-0.1.0a1/sleight/core/human/timing.py +185 -0
- sleight-0.1.0a1/sleight/core/human/wind.py +145 -0
- sleight-0.1.0a1/sleight/core/input.py +363 -0
- sleight-0.1.0a1/sleight/core/keymap.py +285 -0
- sleight-0.1.0a1/sleight/core/netidle.py +131 -0
- sleight-0.1.0a1/sleight/core/protocol.py +100 -0
- sleight-0.1.0a1/sleight/core/session.py +557 -0
- sleight-0.1.0a1/sleight/core/transport.py +360 -0
- sleight-0.1.0a1/sleight/core/types.py +191 -0
- sleight-0.1.0a1/sleight/lease/__init__.py +11 -0
- sleight-0.1.0a1/sleight/lease/base.py +81 -0
- sleight-0.1.0a1/sleight/lease/memory.py +78 -0
- sleight-0.1.0a1/sleight/pool.py +506 -0
- sleight-0.1.0a1/sleight/providers/__init__.py +14 -0
- sleight-0.1.0a1/sleight/providers/base.py +245 -0
- sleight-0.1.0a1/sleight/providers/cloakbrowser.py +490 -0
- sleight-0.1.0a1/sleight/providers/plain.py +115 -0
- sleight-0.1.0a1/sleight/py.typed +0 -0
- sleight-0.1.0a1/tests/__init__.py +0 -0
- sleight-0.1.0a1/tests/conftest.py +249 -0
- sleight-0.1.0a1/tests/test_engine.py +161 -0
- sleight-0.1.0a1/tests/test_human.py +493 -0
- sleight-0.1.0a1/tests/test_input.py +428 -0
- sleight-0.1.0a1/tests/test_integration_interaction.py +246 -0
- sleight-0.1.0a1/tests/test_integration_manager.py +130 -0
- sleight-0.1.0a1/tests/test_keymap.py +171 -0
- sleight-0.1.0a1/tests/test_lease_exclusivity.py +150 -0
- sleight-0.1.0a1/tests/test_logging.py +61 -0
- sleight-0.1.0a1/tests/test_netidle.py +113 -0
- sleight-0.1.0a1/tests/test_pool.py +101 -0
- sleight-0.1.0a1/tests/test_profilespec.py +86 -0
- sleight-0.1.0a1/tests/test_protocol_transport.py +232 -0
- sleight-0.1.0a1/tests/test_providers.py +545 -0
- sleight-0.1.0a1/tests/test_session.py +487 -0
- sleight-0.1.0a1/tests/test_types.py +155 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
|
|
8
|
+
# 虚拟环境
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
|
|
12
|
+
# 工具缓存
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# 编辑器 / IDE —— 里面是开发机的绝对路径
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
|
|
23
|
+
# 本机的 Claude Code 配置(.claude/settings.json 这类共享配置仍然入库)
|
|
24
|
+
.claude/settings.local.json
|
|
25
|
+
|
|
26
|
+
# 凭据
|
|
27
|
+
.env
|
|
28
|
+
*.pem
|
|
29
|
+
*.key
|
|
30
|
+
|
|
31
|
+
# scripts/plot_path.py 的输出
|
|
32
|
+
*.png
|
sleight-0.1.0a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sleight contributors
|
|
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.
|
sleight-0.1.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sleight
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Drive any CDP browser like a human — trajectories, typing rhythm, instance leasing.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yuanqimanong/sleight
|
|
6
|
+
Project-URL: Repository, https://github.com/yuanqimanong/sleight
|
|
7
|
+
Project-URL: Issues, https://github.com/yuanqimanong/sleight/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/yuanqimanong/sleight/blob/main/docs/%E8%AF%A6%E7%BB%86%E6%96%87%E6%A1%A3%E6%89%8B%E5%86%8C.md
|
|
9
|
+
Author: sleight contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: automation,browser,cdp,chrome-devtools-protocol,human-like
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: websocket-client>=1.9
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
26
|
+
Provides-Extra: redis
|
|
27
|
+
Requires-Dist: redis>=5; extra == 'redis'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# sleight
|
|
31
|
+
|
|
32
|
+
**Drive any CDP browser like a human.** Bezier trajectories with real hand tremor,
|
|
33
|
+
typing rhythm modelled on keystroke-dynamics research, and exclusive leasing for
|
|
34
|
+
browser instance pools.
|
|
35
|
+
|
|
36
|
+
Python ≥ 3.11 · one runtime dependency (`websocket-client`) · MIT
|
|
37
|
+
|
|
38
|
+
📖 **[中文文档手册](docs/详细文档手册.md)** — 安装、快速开始、实战场景、CloakBrowser Manager 部署
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install sleight
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 30 seconds
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from sleight import connect, Text
|
|
48
|
+
|
|
49
|
+
with connect("http://127.0.0.1:9222") as s: # opens its own tab, closes it on exit
|
|
50
|
+
s.open("https://example.com", wait=Text("Example Domain"))
|
|
51
|
+
print(s.title(), len(s.content()))
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With a browser pool that has real profiles behind it:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from sleight.providers import CloakBrowserManager
|
|
58
|
+
|
|
59
|
+
mgr = CloakBrowserManager("http://127.0.0.1:19000", token="…")
|
|
60
|
+
|
|
61
|
+
with mgr.lease() as inst: # exclusive lease, released on exit
|
|
62
|
+
with inst.session(human=True) as s: # every action gets a human trajectory
|
|
63
|
+
s.open("https://example.com")
|
|
64
|
+
s.click("#login")
|
|
65
|
+
s.type("#email", "user@example.com")
|
|
66
|
+
s.click("#submit", human=False) # …except this one, speed matters here
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Target one specific profile — by id, by name, or by tag:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
with mgr.lease(instance_id="5edcc28a-…") as inst: ...
|
|
73
|
+
with mgr.lease(where=lambda i: i.name == "Win-US-02") as inst: ...
|
|
74
|
+
with mgr.lease(where=lambda i: "us" in i.tags) as inst: ...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Three providers' worth of instances, one logical pool:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from sleight import Pool
|
|
81
|
+
from sleight.providers import CloakBrowserManager, Plain
|
|
82
|
+
|
|
83
|
+
pool = Pool([
|
|
84
|
+
CloakBrowserManager("http://10.0.0.1:9000", token=T1, name="hk"),
|
|
85
|
+
CloakBrowserManager("http://10.0.0.2:9000", token=T2, name="sg"),
|
|
86
|
+
Plain("http://127.0.0.1:9222", name="local"),
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
with pool.lease(where=lambda i: "us" in i.tags) as inst:
|
|
90
|
+
...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Why this exists
|
|
94
|
+
|
|
95
|
+
Fingerprint-level anti-detection is a solved problem — CloakBrowser patches Chromium
|
|
96
|
+
at the source level, Camoufox patches Firefox. They fix **what the browser looks like**.
|
|
97
|
+
Nothing fixes **how it moves**.
|
|
98
|
+
|
|
99
|
+
- Playwright and Puppeteer teleport the mouse. `mouse.move(steps=N)` interpolates a
|
|
100
|
+
**straight line at constant speed** — zero jitter, zero acceleration. That is itself
|
|
101
|
+
a signature.
|
|
102
|
+
- The browser will not fill in the trajectory for you. Even with a humanize feature
|
|
103
|
+
enabled browser-side, an external CDP client produces **zero** `mousemove` events
|
|
104
|
+
between press and release. Measured, not assumed.
|
|
105
|
+
- The good trajectory work lives in JavaScript (`ghost-cursor`). Python ports are
|
|
106
|
+
thinly maintained.
|
|
107
|
+
- Crawlee for Python's `BrowserPool` [does not support remote browsers](https://github.com/apify/crawlee-python/issues/1743).
|
|
108
|
+
|
|
109
|
+
sleight fills exactly that gap: **Python + remote CDP + human behaviour + instance leasing.**
|
|
110
|
+
|
|
111
|
+
## Relationship to Playwright
|
|
112
|
+
|
|
113
|
+
**Not a replacement — a complement.** sleight is a driver layer, not a framework.
|
|
114
|
+
It deliberately does not do iframes/OOPIF, downloads, video, tracing, or a full
|
|
115
|
+
locator DSL. When you need those, use Playwright.
|
|
116
|
+
|
|
117
|
+
The interesting part is that you can use both: sleight's `human` module is
|
|
118
|
+
[sans-io](https://sans-io.readthedocs.io/) — it emits `(method, params, sleep_after)`
|
|
119
|
+
tuples and never touches a socket — so it drives a Playwright `CDPSession` just as
|
|
120
|
+
happily as sleight's own transport.
|
|
121
|
+
|
|
122
|
+
## What makes the motion credible
|
|
123
|
+
|
|
124
|
+
| | sleight | typical automation |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| Path shape | cubic Bezier, control points offset to one side | straight line |
|
|
127
|
+
| Micro-motion | WindMouse wind term (correlated tremor) | none, or white noise |
|
|
128
|
+
| Point count | Fitts's law — far small targets take longer | fixed `steps=N` |
|
|
129
|
+
| Landing | truncated Gaussian inside the box | dead centre |
|
|
130
|
+
| Coordinates | integers | floats used as "jitter" |
|
|
131
|
+
| Overshoot | past the target then back, distance-scaled | exact arrival |
|
|
132
|
+
| Typing | per-character events, interval by digraph class | one `insertText` |
|
|
133
|
+
| Scrolling | repeated small `mouseWheel` deltas | one `scrollTo` |
|
|
134
|
+
|
|
135
|
+
Parameters are not invented. They come from the
|
|
136
|
+
[WindMouse](https://ben.land/post/2021/04/25/windmouse-human-mouse-movement/) physical
|
|
137
|
+
model, [ghost-cursor](https://github.com/Xetera/ghost-cursor)'s Fitts-law point
|
|
138
|
+
budgeting, and published keystroke-dynamics measurements (alternating-hand digraphs
|
|
139
|
+
average 114 ms, same-hand-different-finger 131 ms, same-finger slowest and most
|
|
140
|
+
variable).
|
|
141
|
+
|
|
142
|
+
## Scope
|
|
143
|
+
|
|
144
|
+
**Does:** navigation and typed wait conditions · rendered-DOM reads · CSS queries ·
|
|
145
|
+
human mouse / keyboard / wheel · instance discovery across providers · cooperative
|
|
146
|
+
exclusive leasing with TTL renewal · idempotent recovery.
|
|
147
|
+
|
|
148
|
+
**Does not:** data extraction · scheduling and queues · fingerprint spoofing (that is
|
|
149
|
+
the browser's job) · iframe / OOPIF / Shadow DOM piercing · strict fencing · WebDriver
|
|
150
|
+
BiDi · Firefox.
|
|
151
|
+
|
|
152
|
+
## Status
|
|
153
|
+
|
|
154
|
+
`0.1.0a1` — alpha, the API will move. Every release documents its breaking changes.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# sleight
|
|
2
|
+
|
|
3
|
+
**Drive any CDP browser like a human.** Bezier trajectories with real hand tremor,
|
|
4
|
+
typing rhythm modelled on keystroke-dynamics research, and exclusive leasing for
|
|
5
|
+
browser instance pools.
|
|
6
|
+
|
|
7
|
+
Python ≥ 3.11 · one runtime dependency (`websocket-client`) · MIT
|
|
8
|
+
|
|
9
|
+
📖 **[中文文档手册](docs/详细文档手册.md)** — 安装、快速开始、实战场景、CloakBrowser Manager 部署
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install sleight
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 30 seconds
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from sleight import connect, Text
|
|
19
|
+
|
|
20
|
+
with connect("http://127.0.0.1:9222") as s: # opens its own tab, closes it on exit
|
|
21
|
+
s.open("https://example.com", wait=Text("Example Domain"))
|
|
22
|
+
print(s.title(), len(s.content()))
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
With a browser pool that has real profiles behind it:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from sleight.providers import CloakBrowserManager
|
|
29
|
+
|
|
30
|
+
mgr = CloakBrowserManager("http://127.0.0.1:19000", token="…")
|
|
31
|
+
|
|
32
|
+
with mgr.lease() as inst: # exclusive lease, released on exit
|
|
33
|
+
with inst.session(human=True) as s: # every action gets a human trajectory
|
|
34
|
+
s.open("https://example.com")
|
|
35
|
+
s.click("#login")
|
|
36
|
+
s.type("#email", "user@example.com")
|
|
37
|
+
s.click("#submit", human=False) # …except this one, speed matters here
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Target one specific profile — by id, by name, or by tag:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
with mgr.lease(instance_id="5edcc28a-…") as inst: ...
|
|
44
|
+
with mgr.lease(where=lambda i: i.name == "Win-US-02") as inst: ...
|
|
45
|
+
with mgr.lease(where=lambda i: "us" in i.tags) as inst: ...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Three providers' worth of instances, one logical pool:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from sleight import Pool
|
|
52
|
+
from sleight.providers import CloakBrowserManager, Plain
|
|
53
|
+
|
|
54
|
+
pool = Pool([
|
|
55
|
+
CloakBrowserManager("http://10.0.0.1:9000", token=T1, name="hk"),
|
|
56
|
+
CloakBrowserManager("http://10.0.0.2:9000", token=T2, name="sg"),
|
|
57
|
+
Plain("http://127.0.0.1:9222", name="local"),
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
with pool.lease(where=lambda i: "us" in i.tags) as inst:
|
|
61
|
+
...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Why this exists
|
|
65
|
+
|
|
66
|
+
Fingerprint-level anti-detection is a solved problem — CloakBrowser patches Chromium
|
|
67
|
+
at the source level, Camoufox patches Firefox. They fix **what the browser looks like**.
|
|
68
|
+
Nothing fixes **how it moves**.
|
|
69
|
+
|
|
70
|
+
- Playwright and Puppeteer teleport the mouse. `mouse.move(steps=N)` interpolates a
|
|
71
|
+
**straight line at constant speed** — zero jitter, zero acceleration. That is itself
|
|
72
|
+
a signature.
|
|
73
|
+
- The browser will not fill in the trajectory for you. Even with a humanize feature
|
|
74
|
+
enabled browser-side, an external CDP client produces **zero** `mousemove` events
|
|
75
|
+
between press and release. Measured, not assumed.
|
|
76
|
+
- The good trajectory work lives in JavaScript (`ghost-cursor`). Python ports are
|
|
77
|
+
thinly maintained.
|
|
78
|
+
- Crawlee for Python's `BrowserPool` [does not support remote browsers](https://github.com/apify/crawlee-python/issues/1743).
|
|
79
|
+
|
|
80
|
+
sleight fills exactly that gap: **Python + remote CDP + human behaviour + instance leasing.**
|
|
81
|
+
|
|
82
|
+
## Relationship to Playwright
|
|
83
|
+
|
|
84
|
+
**Not a replacement — a complement.** sleight is a driver layer, not a framework.
|
|
85
|
+
It deliberately does not do iframes/OOPIF, downloads, video, tracing, or a full
|
|
86
|
+
locator DSL. When you need those, use Playwright.
|
|
87
|
+
|
|
88
|
+
The interesting part is that you can use both: sleight's `human` module is
|
|
89
|
+
[sans-io](https://sans-io.readthedocs.io/) — it emits `(method, params, sleep_after)`
|
|
90
|
+
tuples and never touches a socket — so it drives a Playwright `CDPSession` just as
|
|
91
|
+
happily as sleight's own transport.
|
|
92
|
+
|
|
93
|
+
## What makes the motion credible
|
|
94
|
+
|
|
95
|
+
| | sleight | typical automation |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| Path shape | cubic Bezier, control points offset to one side | straight line |
|
|
98
|
+
| Micro-motion | WindMouse wind term (correlated tremor) | none, or white noise |
|
|
99
|
+
| Point count | Fitts's law — far small targets take longer | fixed `steps=N` |
|
|
100
|
+
| Landing | truncated Gaussian inside the box | dead centre |
|
|
101
|
+
| Coordinates | integers | floats used as "jitter" |
|
|
102
|
+
| Overshoot | past the target then back, distance-scaled | exact arrival |
|
|
103
|
+
| Typing | per-character events, interval by digraph class | one `insertText` |
|
|
104
|
+
| Scrolling | repeated small `mouseWheel` deltas | one `scrollTo` |
|
|
105
|
+
|
|
106
|
+
Parameters are not invented. They come from the
|
|
107
|
+
[WindMouse](https://ben.land/post/2021/04/25/windmouse-human-mouse-movement/) physical
|
|
108
|
+
model, [ghost-cursor](https://github.com/Xetera/ghost-cursor)'s Fitts-law point
|
|
109
|
+
budgeting, and published keystroke-dynamics measurements (alternating-hand digraphs
|
|
110
|
+
average 114 ms, same-hand-different-finger 131 ms, same-finger slowest and most
|
|
111
|
+
variable).
|
|
112
|
+
|
|
113
|
+
## Scope
|
|
114
|
+
|
|
115
|
+
**Does:** navigation and typed wait conditions · rendered-DOM reads · CSS queries ·
|
|
116
|
+
human mouse / keyboard / wheel · instance discovery across providers · cooperative
|
|
117
|
+
exclusive leasing with TTL renewal · idempotent recovery.
|
|
118
|
+
|
|
119
|
+
**Does not:** data extraction · scheduling and queues · fingerprint spoofing (that is
|
|
120
|
+
the browser's job) · iframe / OOPIF / Shadow DOM piercing · strict fencing · WebDriver
|
|
121
|
+
BiDi · Firefox.
|
|
122
|
+
|
|
123
|
+
## Status
|
|
124
|
+
|
|
125
|
+
`0.1.0a1` — alpha, the API will move. Every release documents its breaking changes.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|