netconverter-cli 0.4.0__tar.gz → 0.4.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 (59) hide show
  1. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/CHANGELOG.md +9 -0
  2. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/PKG-INFO +1 -1
  3. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/pyproject.toml +1 -1
  4. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/__init__.py +1 -1
  5. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/clerk.py +5 -2
  6. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/cli.py +3 -2
  7. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/setup.py +3 -2
  8. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/conftest.py +9 -0
  9. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_clerk.py +15 -0
  10. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_setup.py +1 -1
  11. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_signin_default.py +20 -2
  12. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/.gitignore +0 -0
  13. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/LICENSE +0 -0
  14. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/QUICK_START.md +0 -0
  15. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/README.md +0 -0
  16. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/SECURITY.md +0 -0
  17. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/COMMANDS.md +0 -0
  18. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/PRIVACY.md +0 -0
  19. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/RELEASING.md +0 -0
  20. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/VERIFICATION.md +0 -0
  21. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/README.md +0 -0
  22. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/chatgpt.md +0 -0
  23. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/claude-ai.md +0 -0
  24. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/claude-code.md +0 -0
  25. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/claude-desktop.md +0 -0
  26. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/getting-access.md +0 -0
  27. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/other-assistants.md +0 -0
  28. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/examples/README.md +0 -0
  29. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/activity.py +0 -0
  30. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/api.py +0 -0
  31. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/backend.py +0 -0
  32. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/branding.py +0 -0
  33. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/catalog.py +0 -0
  34. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/commands.py +0 -0
  35. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/connect.py +0 -0
  36. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/controller.py +0 -0
  37. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/credentials.py +0 -0
  38. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/guided.py +0 -0
  39. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/mcp_server.py +0 -0
  40. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/palette.py +0 -0
  41. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/presentation.py +0 -0
  42. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/privacy.py +0 -0
  43. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/providers.py +0 -0
  44. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/remote_mcp.py +0 -0
  45. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/tui.py +0 -0
  46. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/update.py +0 -0
  47. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/workspace.py +0 -0
  48. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_boundaries.py +0 -0
  49. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_clerk_host_binding.py +0 -0
  50. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_cli_020.py +0 -0
  51. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_connect_030.py +0 -0
  52. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_connect_031.py +0 -0
  53. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_download_analysis.py +0 -0
  54. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_inline.py +0 -0
  55. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_mcp.py +0 -0
  56. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_presentation.py +0 -0
  57. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_protocols.py +0 -0
  58. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_sessions.py +0 -0
  59. {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_update.py +0 -0
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 — 2026-10-22
4
+
5
+ - **First-run setup honours the Clerk sign-in (#6).** `NETCONVERTER_API_KEY` in the
6
+ environment no longer skips the sign-in choice and is never copied into the OS keyring.
7
+ `whoami` reports `api_key_stored` for the keyring only and adds `api_key_in_environment`.
8
+ - **Sign-in errors show Clerk's full reason.** The OAuth error description was cut at
9
+ 200 characters, hiding the cause sentence (e.g. a missing `device_code` grant, #684).
10
+ - **`whoami` shows one label per organization** (name, else slug) instead of the raw `org_...` id.
11
+
3
12
  ## 0.4.0 — 2026-10-21
4
13
 
5
14
  - **On PyPI as `netconverter-cli`.** `uv tool install --python 3.12 netconverter-cli`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: netconverter-cli
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: NetConverter CLI — any-vendor network configuration migration, validation and optimization, and a local MCP bridge for your AI assistant
5
5
  Project-URL: Homepage, https://netconverter.ai
6
6
  Project-URL: Repository, https://github.com/valeronlabs/netconverter_terminals
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "netconverter-cli"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "NetConverter CLI — any-vendor network configuration migration, validation and optimization, and a local MCP bridge for your AI assistant"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """NetConverter CLI: file transfer and conversation are separate paths."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.1"
@@ -180,7 +180,8 @@ class OAuthError(ClerkError):
180
180
  def __init__(self, error: str, body=None):
181
181
  description = ""
182
182
  if isinstance(body, dict):
183
- description = _safe_text(body.get("error_description"))
183
+ # Clerk puts the cause in the last sentence; keep the whole description, not a cut-off head.
184
+ description = _safe_text(body.get("error_description"), 600)
184
185
  super().__init__(
185
186
  f"Sign-in refused: {error}" + (f" ({description})" if description else "")
186
187
  )
@@ -567,9 +568,11 @@ def profile_summary(info: dict) -> dict:
567
568
  if isinstance(info.get(key), str) and info[key]:
568
569
  out[key] = _safe_text(info[key], 120)
569
570
  orgs = []
570
- for key in ("org_id", "org_slug", "org_name"):
571
+ # One label for the active organization: its name, else its slug; the raw id only as a last resort.
572
+ for key in ("org_name", "org_slug", "org_id"):
571
573
  if isinstance(info.get(key), str) and info[key]:
572
574
  orgs.append(_safe_text(info[key], 120))
575
+ break
573
576
  for org in info.get("organizations") or info.get("orgs") or []:
574
577
  if isinstance(org, dict):
575
578
  label = org.get("name") or org.get("slug") or org.get("id")
@@ -8,7 +8,7 @@ import sys
8
8
  from pathlib import Path
9
9
  from .api import Client, DEFAULT_BASE, APIError
10
10
  from .backend import detect_profile, open_client
11
- from .credentials import get_secret, keyring_available, save_secret
11
+ from .credentials import get_secret, keyring_available, load_secret, save_secret
12
12
  from .controller import Controller
13
13
  from .providers import Provider, ProviderError
14
14
  from .commands import execute
@@ -139,7 +139,8 @@ def logout(args):
139
139
  def whoami(args):
140
140
  from .clerk import ClerkError, profile_summary
141
141
 
142
- out = {"server": args.server, "api_key_stored": bool(get_secret(account(args.server), prompt=False))}
142
+ out = {"server": args.server, "api_key_stored": bool(load_secret(account(args.server))),
143
+ "api_key_in_environment": bool(os.environ.get("NETCONVERTER_API_KEY"))}
143
144
  session = _clerk_session()
144
145
  if session is None:
145
146
  out["clerk"] = None
@@ -4,7 +4,7 @@ import json
4
4
  from pathlib import Path
5
5
  from .api import Client, DEFAULT_BASE
6
6
  from .backend import open_client
7
- from .credentials import get_secret, save_secret
7
+ from .credentials import get_secret, load_secret, save_secret
8
8
  from .providers import Provider, ProviderError
9
9
  from .workspace import Workspace, WorkspaceError
10
10
 
@@ -85,7 +85,8 @@ class AccountSignIn:
85
85
 
86
86
  def connect_credential(server, signin, ask, secret):
87
87
  """A stored key, the existing sign-in, or the user's choice of signing in or pasting a key."""
88
- saved = get_secret(account(server), prompt=False)
88
+ # Keyring only: a key from the environment must neither skip the sign-in nor be copied into the keyring.
89
+ saved = load_secret(account(server))
89
90
  if not saved and signin.allowed(server):
90
91
  session = signin.session()
91
92
  if session is not None:
@@ -20,3 +20,12 @@ def _no_real_sign_in(request, monkeypatch):
20
20
  monkeypatch.setattr(update, "cache_dir", lambda: cache)
21
21
  if "vault" not in request.fixturenames:
22
22
  monkeypatch.setattr(cli, "_clerk_session", lambda: None)
23
+
24
+
25
+ @pytest.fixture(autouse=True)
26
+ def _no_real_stored_key(request, monkeypatch):
27
+ """First-run setup never sees an API key stored in the developer's real keyring."""
28
+ if "vault" not in request.fixturenames:
29
+ from netconverter_cli import setup
30
+
31
+ monkeypatch.setattr(setup, "load_secret", lambda name: "")
@@ -398,3 +398,18 @@ def test_every_token_request_names_the_mcp_resource():
398
398
 
399
399
  src = inspect.getsource(clerk)
400
400
  assert src.count('"resource": RESOURCE') >= 4
401
+
402
+
403
+ def test_oauth_error_keeps_the_cause_sentence_of_a_long_description():
404
+ """#684: Clerk's cause was cut off at 200 characters."""
405
+ from netconverter_cli.clerk import OAuthError
406
+ cause = "This OAuth application does not have the 'urn:ietf:params:oauth:grant-type:device_code' grant."
407
+ exc = OAuthError("unauthorized_client", {"error_description": "x" * 230 + " " + cause})
408
+ assert cause in str(exc)
409
+
410
+
411
+ def test_profile_summary_shows_the_org_name_not_its_raw_id():
412
+ from netconverter_cli.clerk import profile_summary
413
+ info = {"org_id": "org_39VBTisVKmTSYHZamQtGhl8y9rS", "org_slug": "nexthopllc", "org_name": "NextHop LLC"}
414
+ assert profile_summary(info)["organizations"] == ["NextHop LLC"]
415
+ assert profile_summary({"org_id": "org_X"})["organizations"] == ["org_X"]
@@ -106,7 +106,7 @@ def test_deleted_working_folder_gives_a_clear_message(tmp_path, monkeypatch, cap
106
106
  gone.mkdir()
107
107
  monkeypatch.chdir(gone)
108
108
  gone.rmdir()
109
- monkeypatch.setattr(cli, 'get_secret', Mock(return_value=''))
109
+ monkeypatch.setattr(cli, 'load_secret', Mock(return_value=''))
110
110
  monkeypatch.setattr(cli, '_clerk_session', Mock(return_value=None))
111
111
  cli.main(['whoami']) # account commands need no workspace
112
112
  assert '"api_key_stored": false' in capsys.readouterr().out
@@ -121,7 +121,7 @@ def _setup_mocks(monkeypatch):
121
121
  client = Mock()
122
122
  factory = Mock(return_value=(client, "external"))
123
123
  monkeypatch.setattr(setup, "open_client", factory)
124
- monkeypatch.setattr(setup, "get_secret", lambda name, prompt=True: "")
124
+ monkeypatch.setattr(setup, "load_secret", lambda name: "")
125
125
  save = Mock(return_value=True)
126
126
  monkeypatch.setattr(setup, "save_secret", save)
127
127
  return client, factory, save
@@ -172,7 +172,7 @@ def test_setup_never_offers_sign_in_to_an_untrusted_server(tmp_path, monkeypatch
172
172
  def test_plain_netconverter_in_a_new_folder_reaches_the_app_signed_in(tmp_path, monkeypatch):
173
173
  session = FakeSession()
174
174
  monkeypatch.setattr(cli, "_clerk_session", lambda: session)
175
- monkeypatch.setattr(setup, "get_secret", lambda name, prompt=True: "")
175
+ monkeypatch.setattr(setup, "load_secret", lambda name: "")
176
176
  monkeypatch.setattr(setup, "save_secret", Mock(return_value=True))
177
177
  client = Mock()
178
178
  factory = Mock(return_value=(client, "external"))
@@ -186,3 +186,21 @@ def test_plain_netconverter_in_a_new_folder_reaches_the_app_signed_in(tmp_path,
186
186
  cli.main(["--workspace", str(tmp_path)])
187
187
  factory.assert_called_once_with(session, PROD)
188
188
  ran.assert_called_once()
189
+
190
+
191
+ def test_env_key_neither_skips_the_sign_in_nor_is_copied_to_the_keyring(monkeypatch):
192
+ """#6: NETCONVERTER_API_KEY in the environment is not a stored key."""
193
+ session = FakeSession()
194
+ monkeypatch.setenv("NETCONVERTER_API_KEY", "nck_FROM_ENV")
195
+ monkeypatch.setattr(setup, "load_secret", lambda name: "")
196
+ monkeypatch.setattr(setup, "get_secret", lambda *a, **k: pytest.fail("env key must not be read"))
197
+ signin = Mock(allowed=Mock(return_value=True), session=Mock(return_value=session))
198
+ assert setup.connect_credential(PROD, signin, ask=Mock(), secret=Mock()) is session
199
+
200
+
201
+ def test_blank_key_prompt_does_not_adopt_the_env_key(monkeypatch):
202
+ monkeypatch.setenv("NETCONVERTER_API_KEY", "nck_FROM_ENV")
203
+ monkeypatch.setattr(setup, "load_secret", lambda name: "")
204
+ signin = Mock(allowed=Mock(return_value=False))
205
+ with pytest.raises(ValueError):
206
+ setup.connect_credential(PROD, signin, ask=Mock(), secret=lambda prompt: "")