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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: uno-cli
3
- Version: 1.0.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uno-cli"
3
- version = "1.0.3"
3
+ version = "1.0.4"
4
4
  description = "Uno CLI — Agent tool gateway by ClawdChat. Access 2000+ real-world tools from the command line."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -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
- hint = _match_hint(str(detail))
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 result.get("auth_url"):
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