lightnow-cli 1.0.0__tar.gz → 1.0.1__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.
Files changed (39) hide show
  1. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/PKG-INFO +3 -1
  2. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/README.md +2 -0
  3. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/docs/release.md +2 -2
  4. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/__init__.py +1 -1
  5. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/commands/auth.py +26 -9
  6. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/pyproject.toml +1 -1
  7. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/tests/test_auth_flow.py +20 -3
  8. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/tests/test_cli.py +1 -1
  9. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/.github/workflows/ci.yml +0 -0
  10. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/.github/workflows/release.yml +0 -0
  11. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/.gitignore +0 -0
  12. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/CHANGELOG.md +0 -0
  13. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/CONTRIBUTING.md +0 -0
  14. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/LICENSE +0 -0
  15. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/Makefile +0 -0
  16. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/SECURITY.md +0 -0
  17. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/docs/development.md +0 -0
  18. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/examples/README.md +0 -0
  19. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/examples/docs.md +0 -0
  20. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/examples/openapi.json +0 -0
  21. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/examples/server.json +0 -0
  22. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/authenticated_http.py +0 -0
  23. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/client.py +0 -0
  24. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/commands/__init__.py +0 -0
  25. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/commands/integrations.py +0 -0
  26. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/commands/publish.py +0 -0
  27. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/commands/query.py +0 -0
  28. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/commands/runner.py +0 -0
  29. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/commands/validate.py +0 -0
  30. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/config.py +0 -0
  31. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/main.py +0 -0
  32. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/tls.py +0 -0
  33. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/lightnow_cli/validation.py +0 -0
  34. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/tests/__init__.py +0 -0
  35. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/tests/test_client.py +0 -0
  36. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/tests/test_config.py +0 -0
  37. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/tests/test_integrations.py +0 -0
  38. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/tests/test_tls.py +0 -0
  39. {lightnow_cli-1.0.0 → lightnow_cli-1.0.1}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lightnow-cli
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows
5
5
  Project-URL: Homepage, https://github.com/lightnow-ai/lightnow-cli
6
6
  Project-URL: Repository, https://github.com/lightnow-ai/lightnow-cli
@@ -42,6 +42,8 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  # LightNow CLI
44
44
 
45
+ [![PyPI](https://img.shields.io/pypi/v/lightnow-cli.svg)](https://pypi.org/project/lightnow-cli/)
46
+
45
47
  The **LightNow CLI** connects your local MCP clients with LightNow.
46
48
 
47
49
  Use it to discover MCP servers, sync client configurations, and run
@@ -1,5 +1,7 @@
1
1
  # LightNow CLI
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/lightnow-cli.svg)](https://pypi.org/project/lightnow-cli/)
4
+
3
5
  The **LightNow CLI** connects your local MCP clients with LightNow.
4
6
 
5
7
  Use it to discover MCP servers, sync client configurations, and run
@@ -31,8 +31,8 @@ Before publishing a release:
31
31
  - Environment: `pypi`
32
32
  6. Create a signed or otherwise traceable Git tag:
33
33
  ```bash
34
- git tag v1.0.0
35
- git push origin v1.0.0
34
+ git tag vX.Y.Z
35
+ git push origin vX.Y.Z
36
36
  ```
37
37
  7. Verify the GitHub Actions `Release` workflow:
38
38
  - tests pass,
@@ -1,3 +1,3 @@
1
1
  """LightNow CLI - A command-line tool for managing MCP servers."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.1"
@@ -25,6 +25,7 @@ from ..config import (
25
25
  console = Console()
26
26
  app = typer.Typer(help="Authentication commands")
27
27
 
28
+ AUTH_HTTP_TIMEOUT = 30.0
28
29
  ACCESS_TOKEN_EXPIRED_MESSAGE = (
29
30
  "Access token has expired. Please run 'lightnow login' again."
30
31
  )
@@ -41,6 +42,14 @@ class AccessTokenExpired(AuthError):
41
42
  """The stored access token is expired and cannot be refreshed."""
42
43
 
43
44
 
45
+ def describe_http_error(error: httpx.HTTPError) -> str:
46
+ """Return a useful message for httpx errors that may stringify empty."""
47
+ message = str(error).strip()
48
+ if message:
49
+ return message
50
+ return error.__class__.__name__
51
+
52
+
44
53
  def open_authorization_url(verification_uri: str) -> bool:
45
54
  """Open the device authorization URL in the user's browser."""
46
55
  return webbrowser.open(verification_uri, new=2, autoraise=True)
@@ -59,7 +68,7 @@ def pkce_challenge(verifier: str) -> str:
59
68
 
60
69
  async def discover_oidc_endpoints(issuer: str) -> Dict[str, str]:
61
70
  """Discover the OIDC endpoints required by the CLI."""
62
- async with httpx.AsyncClient() as client:
71
+ async with httpx.AsyncClient(timeout=AUTH_HTTP_TIMEOUT) as client:
63
72
  try:
64
73
  discovery_response = await client.get(
65
74
  f"{issuer}/.well-known/openid-configuration"
@@ -69,7 +78,9 @@ async def discover_oidc_endpoints(issuer: str) -> Dict[str, str]:
69
78
  if not isinstance(discovery, dict):
70
79
  raise AuthError("OIDC discovery response is not a JSON object")
71
80
  except httpx.HTTPError as e:
72
- raise AuthError(f"Failed to discover OIDC endpoints: {e}") from e
81
+ raise AuthError(
82
+ f"Failed to discover OIDC endpoints: {describe_http_error(e)}"
83
+ ) from e
73
84
 
74
85
  device_authorization_endpoint = discovery.get("device_authorization_endpoint")
75
86
  token_endpoint = discovery.get("token_endpoint")
@@ -91,7 +102,7 @@ async def device_code_flow(issuer: str, client_id: str) -> Dict[str, Any]:
91
102
  """Perform OIDC Device Code Flow authentication."""
92
103
  endpoints = await discover_oidc_endpoints(issuer)
93
104
  code_verifier = generate_pkce_verifier()
94
- async with httpx.AsyncClient() as client:
105
+ async with httpx.AsyncClient(timeout=AUTH_HTTP_TIMEOUT) as client:
95
106
  # Start device authorization
96
107
  try:
97
108
  device_response = await client.post(
@@ -107,7 +118,9 @@ async def device_code_flow(issuer: str, client_id: str) -> Dict[str, Any]:
107
118
  device_data = device_response.json()
108
119
 
109
120
  except httpx.HTTPError as e:
110
- raise AuthError(f"Failed to start device authorization: {e}")
121
+ raise AuthError(
122
+ f"Failed to start device authorization: {describe_http_error(e)}"
123
+ )
111
124
 
112
125
  # Show user instructions
113
126
  console.print("\n[bold blue]Device Authorization Required[/bold blue]")
@@ -165,7 +178,7 @@ async def device_code_flow(issuer: str, client_id: str) -> Dict[str, Any]:
165
178
  raise AuthError(f"Authorization failed: {error}")
166
179
 
167
180
  except httpx.HTTPError as e:
168
- raise AuthError(f"Failed to poll for token: {e}")
181
+ raise AuthError(f"Failed to poll for token: {describe_http_error(e)}")
169
182
 
170
183
  raise AuthError("Authorization timed out")
171
184
 
@@ -175,7 +188,7 @@ async def refresh_access_token(
175
188
  ) -> Dict[str, Any]:
176
189
  """Refresh the access token using the stored refresh token."""
177
190
  endpoints = await discover_oidc_endpoints(issuer)
178
- async with httpx.AsyncClient() as client:
191
+ async with httpx.AsyncClient(timeout=AUTH_HTTP_TIMEOUT) as client:
179
192
  try:
180
193
  token_response = await client.post(
181
194
  endpoints["token_endpoint"],
@@ -186,7 +199,9 @@ async def refresh_access_token(
186
199
  },
187
200
  )
188
201
  except httpx.HTTPError as e:
189
- raise AuthError(f"Failed to refresh access token: {e}") from e
202
+ raise AuthError(
203
+ f"Failed to refresh access token: {describe_http_error(e)}"
204
+ ) from e
190
205
 
191
206
  if token_response.status_code == 200:
192
207
  token_data = token_response.json()
@@ -239,14 +254,16 @@ async def fetch_user_info(issuer: str, token: str) -> Dict[str, Any]:
239
254
  if not userinfo_endpoint:
240
255
  raise AuthError("OIDC provider does not expose a userinfo endpoint.")
241
256
 
242
- async with httpx.AsyncClient() as client:
257
+ async with httpx.AsyncClient(timeout=AUTH_HTTP_TIMEOUT) as client:
243
258
  try:
244
259
  response = await client.get(
245
260
  userinfo_endpoint,
246
261
  headers={"Authorization": f"Bearer {token}"},
247
262
  )
248
263
  except httpx.HTTPError as exc:
249
- raise AuthError(f"Failed to fetch user information: {exc}") from exc
264
+ raise AuthError(
265
+ f"Failed to fetch user information: {describe_http_error(exc)}"
266
+ ) from exc
250
267
 
251
268
  if response.status_code == 401:
252
269
  raise AccessTokenExpired(ACCESS_TOKEN_EXPIRED_MESSAGE)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "lightnow-cli"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows"
9
9
  authors = [{name = "LightNow Team"}]
10
10
  license = {text = "Apache-2.0"}
@@ -26,7 +26,8 @@ class FakeResponse:
26
26
  class FakeDeviceClient:
27
27
  """AsyncClient stub that completes device authorization after one poll."""
28
28
 
29
- def __init__(self) -> None:
29
+ def __init__(self, *args: object, **kwargs: object) -> None:
30
+ assert kwargs.get("timeout") == auth.AUTH_HTTP_TIMEOUT
30
31
  self.polls = 0
31
32
 
32
33
  async def __aenter__(self) -> "FakeDeviceClient":
@@ -103,6 +104,11 @@ async def test_device_code_flow_opens_browser_and_returns_token(monkeypatch) ->
103
104
  assert opened == ["https://auth.lightnow.local/device?user_code=TEST"]
104
105
 
105
106
 
107
+ def test_describe_http_error_names_empty_errors() -> None:
108
+ """HTTP errors with empty string output still produce useful messages."""
109
+ assert auth.describe_http_error(auth.httpx.ReadTimeout("")) == "ReadTimeout"
110
+
111
+
106
112
  def test_get_user_info_decodes_unverified_jwt_claims_for_refresh_hints() -> None:
107
113
  """The CLI can inspect local JWT metadata without treating it as identity."""
108
114
  token = (
@@ -135,6 +141,9 @@ async def test_fetch_user_info_uses_oidc_userinfo_endpoint(monkeypatch) -> None:
135
141
  """Identity is fetched from the issuer instead of trusting local JWT claims."""
136
142
 
137
143
  class FakeUserInfoClient:
144
+ def __init__(self, *args: object, **kwargs: object) -> None:
145
+ assert kwargs.get("timeout") == auth.AUTH_HTTP_TIMEOUT
146
+
138
147
  async def __aenter__(self) -> "FakeUserInfoClient":
139
148
  return self
140
149
 
@@ -209,7 +218,11 @@ async def test_refresh_access_token_returns_new_tokens(monkeypatch) -> None:
209
218
  }
210
219
 
211
220
  monkeypatch.setattr(auth, "discover_oidc_endpoints", endpoints)
212
- monkeypatch.setattr(auth.httpx, "AsyncClient", lambda: fake)
221
+ monkeypatch.setattr(
222
+ auth.httpx,
223
+ "AsyncClient",
224
+ lambda *args, **kwargs: fake,
225
+ )
213
226
 
214
227
  token_data = await auth.refresh_access_token(
215
228
  "https://auth.lightnow.local/realms/lightnow",
@@ -234,7 +247,11 @@ async def test_refresh_access_token_invalid_grant_is_expired(monkeypatch) -> Non
234
247
  }
235
248
 
236
249
  monkeypatch.setattr(auth, "discover_oidc_endpoints", endpoints)
237
- monkeypatch.setattr(auth.httpx, "AsyncClient", lambda: fake)
250
+ monkeypatch.setattr(
251
+ auth.httpx,
252
+ "AsyncClient",
253
+ lambda *args, **kwargs: fake,
254
+ )
238
255
 
239
256
  with pytest.raises(auth.AccessTokenExpired, match="Access token has expired"):
240
257
  await auth.refresh_access_token(
@@ -51,7 +51,7 @@ def test_cli_version(runner):
51
51
  """Test CLI version command."""
52
52
  result = runner.invoke(app, ["--version"])
53
53
  assert result.exit_code == 0
54
- assert "LightNow CLI 1.0.0" in result.stdout
54
+ assert "LightNow CLI 1.0.1" in result.stdout
55
55
 
56
56
 
57
57
  def test_cli_help(runner):
File without changes
File without changes
File without changes
File without changes
File without changes