clawifi 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.
- clawifi-0.1.0/LICENSE +21 -0
- clawifi-0.1.0/PKG-INFO +87 -0
- clawifi-0.1.0/README.md +65 -0
- clawifi-0.1.0/pyproject.toml +42 -0
- clawifi-0.1.0/setup.cfg +4 -0
- clawifi-0.1.0/src/clawifi/__init__.py +13 -0
- clawifi-0.1.0/src/clawifi/cli.py +343 -0
- clawifi-0.1.0/src/clawifi/client.py +336 -0
- clawifi-0.1.0/src/clawifi/errors.py +46 -0
- clawifi-0.1.0/src/clawifi/mcp_server.py +118 -0
- clawifi-0.1.0/src/clawifi/py.typed +1 -0
- clawifi-0.1.0/src/clawifi/testing.py +70 -0
- clawifi-0.1.0/src/clawifi.egg-info/PKG-INFO +87 -0
- clawifi-0.1.0/src/clawifi.egg-info/SOURCES.txt +17 -0
- clawifi-0.1.0/src/clawifi.egg-info/dependency_links.txt +1 -0
- clawifi-0.1.0/src/clawifi.egg-info/entry_points.txt +3 -0
- clawifi-0.1.0/src/clawifi.egg-info/requires.txt +6 -0
- clawifi-0.1.0/src/clawifi.egg-info/top_level.txt +1 -0
- clawifi-0.1.0/tests/test_client.py +96 -0
clawifi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ReARC 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.
|
clawifi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clawifi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK, CLI, and MCP server for Clawifi, the agent-native internet gateway.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://clawifi.qzz.io
|
|
7
|
+
Project-URL: Documentation, https://clawifi.qzz.io/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/atakanelik34/clawifi-mcp
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: httpx>=0.27.2
|
|
17
|
+
Requires-Dist: mcp>=1.2.0
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest>=8.3.3; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# clawifi
|
|
24
|
+
|
|
25
|
+
<!-- mcp-name: io.github.atakanelik34/clawifi-mcp -->
|
|
26
|
+
|
|
27
|
+
Official Python SDK, CLI, and MCP server for [Clawifi](https://clawifi.qzz.io), the agent-native internet gateway.
|
|
28
|
+
|
|
29
|
+
This package is a thin client over the hosted Clawifi API. It does not include the Clawifi server, browser runtime, or Playwright — those run on Clawifi's infrastructure.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install clawifi
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Get an API key
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
clawifi signup --email you@example.com --password "a strong password"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This creates an account, grants 1,000 free credits, and prints an API key. Export it:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export CLAWIFI_API_KEY="clawifi_live_..."
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quicktest
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
clawifi quicktest --url https://example.com
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Use as an MCP server
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
clawifi mcp config
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
prints a ready-to-paste MCP client config. The MCP server itself runs as:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
clawifi-mcp
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
reading `CLAWIFI_API_KEY` from the environment.
|
|
68
|
+
|
|
69
|
+
## Use as a Python SDK
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from clawifi import Clawifi
|
|
73
|
+
|
|
74
|
+
client = Clawifi() # reads CLAWIFI_API_KEY from the environment
|
|
75
|
+
page = client.fetch("https://example.com")
|
|
76
|
+
print(page["success"])
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
An async client is also available as `clawifi.AsyncClawifi`.
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
See [clawifi.qzz.io/docs](https://clawifi.qzz.io/docs) for the full API reference, MCP tool list, and CLI command reference.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
clawifi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# clawifi
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.atakanelik34/clawifi-mcp -->
|
|
4
|
+
|
|
5
|
+
Official Python SDK, CLI, and MCP server for [Clawifi](https://clawifi.qzz.io), the agent-native internet gateway.
|
|
6
|
+
|
|
7
|
+
This package is a thin client over the hosted Clawifi API. It does not include the Clawifi server, browser runtime, or Playwright — those run on Clawifi's infrastructure.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install clawifi
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Get an API key
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
clawifi signup --email you@example.com --password "a strong password"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This creates an account, grants 1,000 free credits, and prints an API key. Export it:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export CLAWIFI_API_KEY="clawifi_live_..."
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quicktest
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
clawifi quicktest --url https://example.com
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Use as an MCP server
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
clawifi mcp config
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
prints a ready-to-paste MCP client config. The MCP server itself runs as:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
clawifi-mcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
reading `CLAWIFI_API_KEY` from the environment.
|
|
46
|
+
|
|
47
|
+
## Use as a Python SDK
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from clawifi import Clawifi
|
|
51
|
+
|
|
52
|
+
client = Clawifi() # reads CLAWIFI_API_KEY from the environment
|
|
53
|
+
page = client.fetch("https://example.com")
|
|
54
|
+
print(page["success"])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
An async client is also available as `clawifi.AsyncClawifi`.
|
|
58
|
+
|
|
59
|
+
## Documentation
|
|
60
|
+
|
|
61
|
+
See [clawifi.qzz.io/docs](https://clawifi.qzz.io/docs) for the full API reference, MCP tool list, and CLI command reference.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "clawifi"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Official Python SDK, CLI, and MCP server for Clawifi, the agent-native internet gateway."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"httpx>=0.27.2",
|
|
10
|
+
"mcp>=1.2.0",
|
|
11
|
+
]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://clawifi.qzz.io"
|
|
21
|
+
Documentation = "https://clawifi.qzz.io/docs"
|
|
22
|
+
Repository = "https://github.com/atakanelik34/clawifi-mcp"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
clawifi = "clawifi.cli:run"
|
|
26
|
+
clawifi-mcp = "clawifi.mcp_server:run"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = [
|
|
30
|
+
"pytest>=8.3.3",
|
|
31
|
+
"pytest-asyncio>=0.24.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["setuptools>=68", "wheel"]
|
|
36
|
+
build-backend = "setuptools.build_meta"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
clawifi = ["py.typed"]
|
clawifi-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from clawifi.client import AsyncClawifi, Clawifi, ClawifiConfig
|
|
4
|
+
from clawifi.errors import ClawifiAPIError, ClawifiError, ClawifiTransportError
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AsyncClawifi",
|
|
8
|
+
"Clawifi",
|
|
9
|
+
"ClawifiAPIError",
|
|
10
|
+
"ClawifiConfig",
|
|
11
|
+
"ClawifiError",
|
|
12
|
+
"ClawifiTransportError",
|
|
13
|
+
]
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_API_BASE = "https://api.clawifi.qzz.io"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(argv: list[str] | None = None) -> None:
|
|
14
|
+
parser = argparse.ArgumentParser(prog="clawifi", description="Clawifi cloud CLI for agent internet reach.")
|
|
15
|
+
parser.add_argument("--api-base", default=os.getenv("CLAWIFI_API_BASE", DEFAULT_API_BASE))
|
|
16
|
+
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
17
|
+
|
|
18
|
+
signup = subcommands.add_parser("signup", help="Create a Clawifi account and first API key.")
|
|
19
|
+
signup.add_argument("--email", required=True)
|
|
20
|
+
signup.add_argument("--password", required=True)
|
|
21
|
+
signup.add_argument("--display-name")
|
|
22
|
+
|
|
23
|
+
login = subcommands.add_parser("login", help="Create an account session token.")
|
|
24
|
+
login.add_argument("--email", required=True)
|
|
25
|
+
login.add_argument("--password", required=True)
|
|
26
|
+
|
|
27
|
+
verify = subcommands.add_parser("verify-email", help="Verify your Clawifi email token and create an API key.")
|
|
28
|
+
verify.add_argument("--token", required=True)
|
|
29
|
+
|
|
30
|
+
quicktest = subcommands.add_parser("quicktest", help="Verify your API key against the Clawifi API.")
|
|
31
|
+
quicktest.add_argument("--url", default="https://example.com")
|
|
32
|
+
|
|
33
|
+
mcp = subcommands.add_parser("mcp", help="Print MCP setup helpers.")
|
|
34
|
+
mcp_subcommands = mcp.add_subparsers(dest="mcp_command", required=True)
|
|
35
|
+
mcp_subcommands.add_parser("config", help="Print a minimal MCP server config.")
|
|
36
|
+
|
|
37
|
+
browser = subcommands.add_parser("browser", help="Manage Clawifi browser runtime sessions.")
|
|
38
|
+
browser_subcommands = browser.add_subparsers(dest="browser_command", required=True)
|
|
39
|
+
browser_pair = browser_subcommands.add_parser("pair", help="Create a short-lived WebBridge pairing code.")
|
|
40
|
+
browser_pair.add_argument(
|
|
41
|
+
"--local-api-base",
|
|
42
|
+
default=os.getenv("CLAWIFI_LOCAL_API_BASE", "http://127.0.0.1:8000"),
|
|
43
|
+
)
|
|
44
|
+
browser_doctor = browser_subcommands.add_parser(
|
|
45
|
+
"doctor", help="Run browser runtime and agent configuration diagnostics."
|
|
46
|
+
)
|
|
47
|
+
browser_doctor.add_argument(
|
|
48
|
+
"--verbose",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Include the complete check report (currently the default output).",
|
|
51
|
+
)
|
|
52
|
+
browser_doctor.add_argument(
|
|
53
|
+
"--local-api-base",
|
|
54
|
+
default=os.getenv("CLAWIFI_LOCAL_API_BASE", "http://127.0.0.1:8000"),
|
|
55
|
+
)
|
|
56
|
+
browser_start = browser_subcommands.add_parser("start", help="Start a managed headed or headless browser session.")
|
|
57
|
+
browser_start.add_argument("--url", required=True)
|
|
58
|
+
browser_start.add_argument("--mode", choices=("headless", "headed"), default="headless")
|
|
59
|
+
browser_start.add_argument("--profile-id")
|
|
60
|
+
browser_start.add_argument("--ttl-seconds", type=int, default=300)
|
|
61
|
+
browser_start.add_argument(
|
|
62
|
+
"--local-api-base",
|
|
63
|
+
default=os.getenv("CLAWIFI_LOCAL_API_BASE", "http://127.0.0.1:8000"),
|
|
64
|
+
)
|
|
65
|
+
browser_status = browser_subcommands.add_parser("status", help="List browser sessions or inspect one session.")
|
|
66
|
+
browser_status.add_argument("session_id", nargs="?")
|
|
67
|
+
browser_status.add_argument(
|
|
68
|
+
"--local-api-base",
|
|
69
|
+
default=os.getenv("CLAWIFI_LOCAL_API_BASE", "http://127.0.0.1:8000"),
|
|
70
|
+
)
|
|
71
|
+
browser_stop = browser_subcommands.add_parser("stop", help="Close one or all browser sessions.")
|
|
72
|
+
browser_stop_target = browser_stop.add_mutually_exclusive_group(required=True)
|
|
73
|
+
browser_stop_target.add_argument("session_id", nargs="?")
|
|
74
|
+
browser_stop_target.add_argument("--all", action="store_true")
|
|
75
|
+
browser_stop.add_argument(
|
|
76
|
+
"--local-api-base",
|
|
77
|
+
default=os.getenv("CLAWIFI_LOCAL_API_BASE", "http://127.0.0.1:8000"),
|
|
78
|
+
)
|
|
79
|
+
browser_smoke = browser_subcommands.add_parser("smoke", help="Create, verify, and always close a browser session.")
|
|
80
|
+
browser_smoke.add_argument("--url", default="https://example.com")
|
|
81
|
+
browser_smoke.add_argument("--mode", choices=("headless", "headed"), default="headless")
|
|
82
|
+
browser_smoke.add_argument("--profile-id")
|
|
83
|
+
browser_smoke.add_argument("--ttl-seconds", type=int, default=300)
|
|
84
|
+
browser_smoke.add_argument(
|
|
85
|
+
"--local-api-base",
|
|
86
|
+
default=os.getenv("CLAWIFI_LOCAL_API_BASE", "http://127.0.0.1:8000"),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
monitors = subcommands.add_parser("monitors", help="Manage Evidence Monitor checks.")
|
|
90
|
+
monitor_subcommands = monitors.add_subparsers(dest="monitor_command", required=True)
|
|
91
|
+
monitor_create = monitor_subcommands.add_parser("create", help="Create a monitor.")
|
|
92
|
+
monitor_create.add_argument("--name", required=True)
|
|
93
|
+
monitor_create.add_argument("--url", required=True)
|
|
94
|
+
monitor_create.add_argument("--interval-seconds", type=int, default=900)
|
|
95
|
+
monitor_create.add_argument("--format", dest="formats", action="append", default=None)
|
|
96
|
+
monitor_create.add_argument("--diff-mode", dest="diff_modes", action="append", default=None)
|
|
97
|
+
monitor_create.add_argument("--json", action="store_true")
|
|
98
|
+
monitor_list = monitor_subcommands.add_parser("list", help="List monitors.")
|
|
99
|
+
monitor_list.add_argument("--include-deleted", action="store_true")
|
|
100
|
+
monitor_list.add_argument("--json", action="store_true")
|
|
101
|
+
monitor_get = monitor_subcommands.add_parser("get", help="Get one monitor.")
|
|
102
|
+
monitor_get.add_argument("monitor_id")
|
|
103
|
+
monitor_get.add_argument("--json", action="store_true")
|
|
104
|
+
monitor_pause = monitor_subcommands.add_parser("pause", help="Pause a monitor.")
|
|
105
|
+
monitor_pause.add_argument("monitor_id")
|
|
106
|
+
monitor_pause.add_argument("--json", action="store_true")
|
|
107
|
+
monitor_resume = monitor_subcommands.add_parser("resume", help="Resume a monitor.")
|
|
108
|
+
monitor_resume.add_argument("monitor_id")
|
|
109
|
+
monitor_resume.add_argument("--run-immediately", action="store_true")
|
|
110
|
+
monitor_resume.add_argument("--json", action="store_true")
|
|
111
|
+
monitor_delete = monitor_subcommands.add_parser("delete", help="Delete a monitor.")
|
|
112
|
+
monitor_delete.add_argument("monitor_id")
|
|
113
|
+
monitor_delete.add_argument("--json", action="store_true")
|
|
114
|
+
monitor_run = monitor_subcommands.add_parser("run", help="Run a monitor now.")
|
|
115
|
+
monitor_run.add_argument("monitor_id")
|
|
116
|
+
monitor_run.add_argument("--json", action="store_true")
|
|
117
|
+
|
|
118
|
+
args = parser.parse_args(argv)
|
|
119
|
+
api_base = args.api_base.rstrip("/")
|
|
120
|
+
|
|
121
|
+
if args.command == "signup":
|
|
122
|
+
payload = _request(
|
|
123
|
+
"POST",
|
|
124
|
+
f"{api_base}/auth/signup",
|
|
125
|
+
json_body={"email": args.email, "password": args.password, "display_name": args.display_name},
|
|
126
|
+
)
|
|
127
|
+
print("Clawifi account created.")
|
|
128
|
+
print(f"Email: {payload['account']['email']}")
|
|
129
|
+
print(f"Credits: {payload['credits']['available']}")
|
|
130
|
+
if payload.get("requires_email_verification"):
|
|
131
|
+
print("Email verification is required before an API key is issued.")
|
|
132
|
+
return
|
|
133
|
+
print("Set this key in your agent runtime:")
|
|
134
|
+
print(f"export CLAWIFI_API_KEY={payload['api_key']}")
|
|
135
|
+
raise SystemExit(0)
|
|
136
|
+
|
|
137
|
+
if args.command == "verify-email":
|
|
138
|
+
payload = _request("POST", f"{api_base}/auth/verify-email", json_body={"token": args.token})
|
|
139
|
+
print("Clawifi email verified.")
|
|
140
|
+
print(f"Email: {payload['account']['email']}")
|
|
141
|
+
print(f"Credits: {payload['credits']['available']}")
|
|
142
|
+
print(f"export CLAWIFI_SESSION_TOKEN={payload['session_token']}")
|
|
143
|
+
print(f"export CLAWIFI_API_KEY={payload['api_key']}")
|
|
144
|
+
raise SystemExit(0)
|
|
145
|
+
|
|
146
|
+
if args.command == "login":
|
|
147
|
+
payload = _request(
|
|
148
|
+
"POST",
|
|
149
|
+
f"{api_base}/auth/login",
|
|
150
|
+
json_body={"email": args.email, "password": args.password},
|
|
151
|
+
)
|
|
152
|
+
print("Clawifi session created.")
|
|
153
|
+
print(f"Session expires: {payload['session_expires_at']}")
|
|
154
|
+
print(f"export CLAWIFI_SESSION_TOKEN={payload['session_token']}")
|
|
155
|
+
raise SystemExit(0)
|
|
156
|
+
|
|
157
|
+
if args.command == "quicktest":
|
|
158
|
+
api_key = _required_api_key()
|
|
159
|
+
payload = _request(
|
|
160
|
+
"POST",
|
|
161
|
+
f"{api_base}/fetch",
|
|
162
|
+
token=api_key,
|
|
163
|
+
json_body={"url": args.url, "timeout_seconds": 15},
|
|
164
|
+
)
|
|
165
|
+
print(json.dumps({"ok": True, "success": payload.get("success"), "source": payload.get("source")}, indent=2))
|
|
166
|
+
raise SystemExit(0)
|
|
167
|
+
|
|
168
|
+
if args.command == "mcp" and args.mcp_command == "config":
|
|
169
|
+
_required_api_key()
|
|
170
|
+
print(json.dumps({
|
|
171
|
+
"mcpServers": {
|
|
172
|
+
"clawifi": {
|
|
173
|
+
"command": "clawifi-mcp",
|
|
174
|
+
"env": {
|
|
175
|
+
"CLAWIFI_API_KEY": "${CLAWIFI_API_KEY}",
|
|
176
|
+
"CLAWIFI_API_BASE": api_base
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}, indent=2))
|
|
181
|
+
raise SystemExit(0)
|
|
182
|
+
|
|
183
|
+
if args.command == "browser":
|
|
184
|
+
api_key = _required_api_key()
|
|
185
|
+
local_api_base = args.local_api_base.rstrip("/")
|
|
186
|
+
if args.browser_command == "pair":
|
|
187
|
+
payload = _request(
|
|
188
|
+
"POST",
|
|
189
|
+
f"{local_api_base}/browser/bridge/pairing",
|
|
190
|
+
token=api_key,
|
|
191
|
+
json_body={},
|
|
192
|
+
)
|
|
193
|
+
print("One-time WebBridge pairing code:")
|
|
194
|
+
print(payload["code"])
|
|
195
|
+
print(f"Expires in {payload['expires_in_seconds']} seconds.")
|
|
196
|
+
print("Paste it into the Clawifi WebBridge extension. It can be used once.")
|
|
197
|
+
elif args.browser_command == "doctor":
|
|
198
|
+
payload = _request("GET", f"{local_api_base}/browser/doctor", token=api_key)
|
|
199
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
200
|
+
if not payload.get("ok", False):
|
|
201
|
+
raise SystemExit(1)
|
|
202
|
+
elif args.browser_command == "start":
|
|
203
|
+
request_payload = {
|
|
204
|
+
"url": args.url,
|
|
205
|
+
"mode": args.mode,
|
|
206
|
+
"ttl_seconds": args.ttl_seconds,
|
|
207
|
+
**({"profile_id": args.profile_id} if args.profile_id else {}),
|
|
208
|
+
}
|
|
209
|
+
payload = _request(
|
|
210
|
+
"POST",
|
|
211
|
+
f"{local_api_base}/browser/sessions",
|
|
212
|
+
token=api_key,
|
|
213
|
+
json_body=request_payload,
|
|
214
|
+
)
|
|
215
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
216
|
+
elif args.browser_command == "status":
|
|
217
|
+
suffix = f"/{args.session_id}" if args.session_id else ""
|
|
218
|
+
payload = _request("GET", f"{local_api_base}/browser/sessions{suffix}", token=api_key)
|
|
219
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
220
|
+
elif args.browser_command == "stop":
|
|
221
|
+
if args.all:
|
|
222
|
+
payload = _request(
|
|
223
|
+
"DELETE",
|
|
224
|
+
f"{local_api_base}/browser/sessions",
|
|
225
|
+
token=api_key,
|
|
226
|
+
json_body={"session_ids": None},
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
payload = _request(
|
|
230
|
+
"DELETE",
|
|
231
|
+
f"{local_api_base}/browser/sessions/{args.session_id}",
|
|
232
|
+
token=api_key,
|
|
233
|
+
)
|
|
234
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
235
|
+
elif args.browser_command == "smoke":
|
|
236
|
+
request_payload = {
|
|
237
|
+
"url": args.url,
|
|
238
|
+
"mode": args.mode,
|
|
239
|
+
"ttl_seconds": args.ttl_seconds,
|
|
240
|
+
**({"profile_id": args.profile_id} if args.profile_id else {}),
|
|
241
|
+
}
|
|
242
|
+
created = _request(
|
|
243
|
+
"POST",
|
|
244
|
+
f"{local_api_base}/browser/sessions",
|
|
245
|
+
token=api_key,
|
|
246
|
+
json_body=request_payload,
|
|
247
|
+
)
|
|
248
|
+
session_id = created.get("session_id")
|
|
249
|
+
cleanup = None
|
|
250
|
+
try:
|
|
251
|
+
if not session_id or created.get("status") != "ready":
|
|
252
|
+
raise SystemExit("Browser smoke failed: session did not become ready")
|
|
253
|
+
finally:
|
|
254
|
+
if session_id:
|
|
255
|
+
cleanup = _request(
|
|
256
|
+
"DELETE",
|
|
257
|
+
f"{local_api_base}/browser/sessions/{session_id}",
|
|
258
|
+
token=api_key,
|
|
259
|
+
)
|
|
260
|
+
print(json.dumps({"ok": True, "session": created, "cleanup": cleanup}, indent=2, sort_keys=True))
|
|
261
|
+
raise SystemExit(0)
|
|
262
|
+
|
|
263
|
+
if args.command == "monitors":
|
|
264
|
+
api_key = _required_api_key()
|
|
265
|
+
if args.monitor_command == "create":
|
|
266
|
+
payload = _request(
|
|
267
|
+
"POST",
|
|
268
|
+
f"{api_base}/monitors",
|
|
269
|
+
token=api_key,
|
|
270
|
+
json_body={
|
|
271
|
+
"name": args.name,
|
|
272
|
+
"url": args.url,
|
|
273
|
+
"schedule": {"interval_seconds": args.interval_seconds},
|
|
274
|
+
"formats": args.formats or ["markdown", "text", "metadata", "links"],
|
|
275
|
+
"diff_modes": args.diff_modes or ["text"],
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
_print_monitor_payload(payload, as_json=args.json)
|
|
279
|
+
elif args.monitor_command == "list":
|
|
280
|
+
suffix = "?include_deleted=true" if args.include_deleted else ""
|
|
281
|
+
payload = _request("GET", f"{api_base}/monitors{suffix}", token=api_key)
|
|
282
|
+
_print_monitor_payload(payload, as_json=args.json)
|
|
283
|
+
elif args.monitor_command == "get":
|
|
284
|
+
payload = _request("GET", f"{api_base}/monitors/{args.monitor_id}", token=api_key)
|
|
285
|
+
_print_monitor_payload(payload, as_json=args.json)
|
|
286
|
+
elif args.monitor_command == "pause":
|
|
287
|
+
payload = _request("POST", f"{api_base}/monitors/{args.monitor_id}/pause", token=api_key, json_body={})
|
|
288
|
+
_print_monitor_payload(payload, as_json=args.json)
|
|
289
|
+
elif args.monitor_command == "resume":
|
|
290
|
+
payload = _request(
|
|
291
|
+
"POST",
|
|
292
|
+
f"{api_base}/monitors/{args.monitor_id}/resume",
|
|
293
|
+
token=api_key,
|
|
294
|
+
json_body={"run_immediately": args.run_immediately},
|
|
295
|
+
)
|
|
296
|
+
_print_monitor_payload(payload, as_json=args.json)
|
|
297
|
+
elif args.monitor_command == "delete":
|
|
298
|
+
payload = _request("DELETE", f"{api_base}/monitors/{args.monitor_id}", token=api_key)
|
|
299
|
+
_print_monitor_payload(payload, as_json=args.json)
|
|
300
|
+
elif args.monitor_command == "run":
|
|
301
|
+
payload = _request("POST", f"{api_base}/monitors/{args.monitor_id}/run", token=api_key)
|
|
302
|
+
_print_monitor_payload(payload, as_json=args.json)
|
|
303
|
+
raise SystemExit(0)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _required_api_key() -> str:
|
|
307
|
+
api_key = os.getenv("CLAWIFI_API_KEY", "").strip()
|
|
308
|
+
if not api_key:
|
|
309
|
+
raise SystemExit("CLAWIFI_API_KEY is required. Run `clawifi signup --email you@example.com --password ...` first.")
|
|
310
|
+
return api_key
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _request(method: str, url: str, *, json_body: dict | None = None, token: str | None = None) -> dict:
|
|
314
|
+
body = json.dumps(json_body or {}, separators=(",", ":")).encode("utf-8") if json_body is not None else None
|
|
315
|
+
headers = {"Content-Type": "application/json", "User-Agent": "clawifi-cli/0.1.0 (+https://clawifi.qzz.io)"}
|
|
316
|
+
if token:
|
|
317
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
318
|
+
request = urllib.request.Request(url, data=body, method=method, headers=headers)
|
|
319
|
+
try:
|
|
320
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
321
|
+
return json.loads(response.read().decode("utf-8"))
|
|
322
|
+
except urllib.error.HTTPError as exc:
|
|
323
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
324
|
+
raise SystemExit(f"Clawifi API error {exc.code}: {detail}") from exc
|
|
325
|
+
except urllib.error.URLError as exc:
|
|
326
|
+
raise SystemExit(f"Clawifi API unreachable: {exc.reason}") from exc
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _print_monitor_payload(payload: dict, *, as_json: bool) -> None:
|
|
330
|
+
if as_json:
|
|
331
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
332
|
+
return
|
|
333
|
+
if "monitors" in payload:
|
|
334
|
+
print(f"Monitors: {payload.get('total', len(payload['monitors']))}")
|
|
335
|
+
for monitor in payload["monitors"]:
|
|
336
|
+
print(f"- {monitor.get('monitor_id')} {monitor.get('status')} {monitor.get('name')}")
|
|
337
|
+
return
|
|
338
|
+
label = payload.get("monitor_id") or payload.get("tick_id") or payload.get("status") or "ok"
|
|
339
|
+
print(f"Monitor result: {label}")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
run()
|