uno-cli 1.0.3__tar.gz → 1.0.4__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.
- {uno_cli-1.0.3 → uno_cli-1.0.4}/PKG-INFO +1 -1
- {uno_cli-1.0.3 → uno_cli-1.0.4}/pyproject.toml +1 -1
- {uno_cli-1.0.3 → uno_cli-1.0.4}/tests/test_cli.py +101 -0
- uno_cli-1.0.4/uno_cli/_version.py +1 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/uno_cli/cli.py +34 -4
- uno_cli-1.0.3/uno_cli/_version.py +0 -1
- {uno_cli-1.0.3 → uno_cli-1.0.4}/.gitignore +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/LICENSE +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/README.md +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/tests/__init__.py +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/tests/test_auth.py +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/tests/test_connections_logs.py +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/tests/test_dry_run.py +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/tests/test_register.py +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/tests/test_scope.py +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/tests/test_token_masking.py +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/uno_cli/__init__.py +0 -0
- {uno_cli-1.0.3 → uno_cli-1.0.4}/uno_cli/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: uno-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Uno CLI — Agent tool gateway by ClawdChat. Access 2000+ real-world tools from the command line.
|
|
5
5
|
Project-URL: Homepage, https://agentools.uno
|
|
6
6
|
Project-URL: Documentation, https://agentools.uno/docs
|
|
@@ -137,6 +137,22 @@ class TestErrorHints:
|
|
|
137
137
|
def test_format_api_error_str_body(self):
|
|
138
138
|
out = cli._format_api_error(500, "server exploded")
|
|
139
139
|
assert out["error"] == "server exploded"
|
|
140
|
+
assert out["success"] is False
|
|
141
|
+
|
|
142
|
+
def test_format_api_error_keeps_gateway_meta(self):
|
|
143
|
+
body = {
|
|
144
|
+
"error": "tool_not_found",
|
|
145
|
+
"message": "Tool not found: x.y",
|
|
146
|
+
"meta": {
|
|
147
|
+
"request_id": "abc",
|
|
148
|
+
"credits_used": 0,
|
|
149
|
+
"charged": False,
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
out = cli._format_api_error(404, body)
|
|
153
|
+
assert out["error"] == "tool_not_found"
|
|
154
|
+
assert out["meta"]["request_id"] == "abc"
|
|
155
|
+
assert out["meta"]["charged"] is False
|
|
140
156
|
|
|
141
157
|
def test_missing_credentials_points_to_login_start(self, capsys, monkeypatch, tmp_path):
|
|
142
158
|
cred_dir = tmp_path / ".uno"
|
|
@@ -155,6 +171,91 @@ class TestErrorHints:
|
|
|
155
171
|
hint = cli._match_hint(msg)
|
|
156
172
|
assert "uno login --start" in hint
|
|
157
173
|
|
|
174
|
+
def test_auth_required_fallback_hint_is_generic_oauth(self):
|
|
175
|
+
"""``_ERROR_HINTS`` keeps a generic OAuth fallback; call path overrides."""
|
|
176
|
+
hint = cli._match_hint("auth_required")
|
|
177
|
+
assert "OAuth required" in hint
|
|
178
|
+
assert "auth_url" in hint
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── cmd_call auth_type hint overrides ──────────────────────────────
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class _CallArgs:
|
|
185
|
+
def __init__(self, tool="demo.tool", args_json=None):
|
|
186
|
+
self.tool = tool
|
|
187
|
+
self.args = args_json
|
|
188
|
+
self.timeout = 60
|
|
189
|
+
self.base_url = "https://clawdtools.uno"
|
|
190
|
+
self.dry_run = False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TestCmdCallAuthHints:
|
|
194
|
+
"""``cmd_call`` must override the generic auth_required hint by auth_type."""
|
|
195
|
+
|
|
196
|
+
def _run_auth_error(self, monkeypatch, capsys, payload):
|
|
197
|
+
monkeypatch.delenv("UNO_ALLOWED_TOOLS", raising=False)
|
|
198
|
+
|
|
199
|
+
class _FakeAPI:
|
|
200
|
+
def post(self, path, data, timeout=60):
|
|
201
|
+
return payload
|
|
202
|
+
|
|
203
|
+
monkeypatch.setattr(cli, "_get_api", lambda args: _FakeAPI())
|
|
204
|
+
with pytest.raises(SystemExit) as ei:
|
|
205
|
+
cli.cmd_call(_CallArgs())
|
|
206
|
+
assert ei.value.code == 1
|
|
207
|
+
return json.loads(capsys.readouterr().out)
|
|
208
|
+
|
|
209
|
+
@pytest.mark.parametrize("auth_type", ["oauth_device", "oauth_device_code", "device_code_api"])
|
|
210
|
+
def test_device_code_auth_hint(self, monkeypatch, capsys, auth_type):
|
|
211
|
+
out = self._run_auth_error(
|
|
212
|
+
monkeypatch,
|
|
213
|
+
capsys,
|
|
214
|
+
{
|
|
215
|
+
"error": "auth_required",
|
|
216
|
+
"auth_type": auth_type,
|
|
217
|
+
"auth_url": "https://example.com/device",
|
|
218
|
+
"get_key_url": "https://example.com/get-key",
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
assert out["error"] == "auth_required"
|
|
222
|
+
assert out["auth_type"] == auth_type
|
|
223
|
+
hint = out["hint"]
|
|
224
|
+
assert "Device-code" in hint
|
|
225
|
+
assert "user code" in hint
|
|
226
|
+
assert "get_key_url" in hint # tells agent NOT to follow it
|
|
227
|
+
assert "Do not ask for a key" in hint
|
|
228
|
+
# Must not steer toward applying for / showing a key.
|
|
229
|
+
assert "Show get_key_url" not in hint
|
|
230
|
+
assert "申请" not in hint
|
|
231
|
+
|
|
232
|
+
def test_generic_oauth_auth_url_hint(self, monkeypatch, capsys):
|
|
233
|
+
out = self._run_auth_error(
|
|
234
|
+
monkeypatch,
|
|
235
|
+
capsys,
|
|
236
|
+
{
|
|
237
|
+
"error": "auth_required",
|
|
238
|
+
"auth_type": "oauth",
|
|
239
|
+
"auth_url": "https://example.com/oauth",
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
assert out["hint"] == "OAuth required. Open auth_url in a browser to authorize."
|
|
243
|
+
assert "Device-code" not in out["hint"]
|
|
244
|
+
assert "user code" not in out["hint"]
|
|
245
|
+
|
|
246
|
+
def test_api_key_auth_hint(self, monkeypatch, capsys):
|
|
247
|
+
out = self._run_auth_error(
|
|
248
|
+
monkeypatch,
|
|
249
|
+
capsys,
|
|
250
|
+
{
|
|
251
|
+
"error": "auth_required",
|
|
252
|
+
"auth_type": "api_key",
|
|
253
|
+
"get_key_url": "https://example.com/get-key",
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
assert "API key required" in out["hint"]
|
|
257
|
+
assert "Show get_key_url" in out["hint"]
|
|
258
|
+
|
|
158
259
|
|
|
159
260
|
# ── JSON output shape of _out ──────────────────────────────────────
|
|
160
261
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.4"
|
|
@@ -494,8 +494,19 @@ def _format_api_error(status, body):
|
|
|
494
494
|
msgs.append(str(err))
|
|
495
495
|
detail = "; ".join(msgs) if msgs else str(detail)
|
|
496
496
|
|
|
497
|
-
result = {"error": str(detail), "status": status}
|
|
498
|
-
|
|
497
|
+
result = {"success": False, "error": str(detail), "status": status}
|
|
498
|
+
if isinstance(body, dict):
|
|
499
|
+
if body.get("message") and str(body.get("message")) != result["error"]:
|
|
500
|
+
result["message"] = body["message"]
|
|
501
|
+
if body.get("meta"):
|
|
502
|
+
result["meta"] = body["meta"]
|
|
503
|
+
if body.get("error") and result["error"] != str(body.get("error")):
|
|
504
|
+
# Prefer the stable gateway code when FastAPI wrapped it.
|
|
505
|
+
code = body.get("error")
|
|
506
|
+
if isinstance(code, str) and code and " " not in code:
|
|
507
|
+
result["error"] = code
|
|
508
|
+
result["message"] = str(detail)
|
|
509
|
+
hint = _match_hint(str(result.get("error") or detail))
|
|
499
510
|
if hint:
|
|
500
511
|
result["hint"] = hint
|
|
501
512
|
return result
|
|
@@ -506,7 +517,7 @@ def _ok(data):
|
|
|
506
517
|
|
|
507
518
|
|
|
508
519
|
def _fail(msg, **extra):
|
|
509
|
-
out = {"error": msg}
|
|
520
|
+
out = {"success": False, "error": msg}
|
|
510
521
|
hint = _match_hint(msg)
|
|
511
522
|
if hint:
|
|
512
523
|
out["hint"] = hint
|
|
@@ -650,6 +661,14 @@ def cmd_tool(args):
|
|
|
650
661
|
_fail("Tool slug required. Usage: uno tool get <slug>")
|
|
651
662
|
try:
|
|
652
663
|
result = api.get(f"/v1/tools/{slug}")
|
|
664
|
+
if isinstance(result, dict) and result.get("error"):
|
|
665
|
+
out = {"success": False, "error": result["error"]}
|
|
666
|
+
if result.get("message"):
|
|
667
|
+
out["message"] = result["message"]
|
|
668
|
+
if result.get("meta"):
|
|
669
|
+
out["meta"] = result["meta"]
|
|
670
|
+
_out(out)
|
|
671
|
+
sys.exit(1)
|
|
653
672
|
if isinstance(result, dict) and "data" in result and "error" in result:
|
|
654
673
|
result = result["data"]
|
|
655
674
|
if result is None:
|
|
@@ -725,8 +744,19 @@ def cmd_call(args):
|
|
|
725
744
|
out[key] = result[key]
|
|
726
745
|
if result.get("meta"):
|
|
727
746
|
out["meta"] = result["meta"]
|
|
747
|
+
else:
|
|
748
|
+
out["meta"] = {
|
|
749
|
+
"credits_used": 0,
|
|
750
|
+
"credits_from": {"free": 0, "paid": 0.0},
|
|
751
|
+
"charged": False,
|
|
752
|
+
}
|
|
728
753
|
auth_type = result.get("auth_type", "")
|
|
729
|
-
if
|
|
754
|
+
if auth_type in ("oauth_device", "oauth_device_code", "device_code_api"):
|
|
755
|
+
out["hint"] = (
|
|
756
|
+
"Device-code OAuth. Open auth_url, enter the user code. "
|
|
757
|
+
"Do not ask for a key or follow get_key_url."
|
|
758
|
+
)
|
|
759
|
+
elif result.get("auth_url"):
|
|
730
760
|
out["hint"] = "OAuth required. Open auth_url in a browser to authorize."
|
|
731
761
|
elif auth_type == "api_key":
|
|
732
762
|
out["hint"] = "API key required. Show get_key_url to the user."
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|