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 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
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()