@agxnte/reidx 2.0.5 → 2.0.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agxnte/reidx",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "Terminal-native personal intelligence and coding CLI (agent-first runtime)",
5
5
  "bin": {
6
6
  "reid": "bin/reidx.js"
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reidx"
3
- version = "2.0.5"
3
+ version = "2.0.6"
4
4
  description = "Terminal-native personal intelligence and coding CLI (agent-first runtime)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,3 +1,3 @@
1
1
  """ReidX: terminal-native agent-first CLI runtime."""
2
2
 
3
- __version__ = "2.0.5"
3
+ __version__ = "2.0.6"
@@ -1,42 +1,75 @@
1
- """Shared HTTP helper for provider clients — stdlib urllib, no extra deps."""
2
- from __future__ import annotations
3
-
4
- import json
5
- import urllib.error
6
- import urllib.request
7
-
8
- TIMEOUT_SECONDS = 120
9
-
10
-
11
- def post_json(url: str, payload: dict, headers: dict[str, str], timeout: int = TIMEOUT_SECONDS) -> dict:
12
- """POST a JSON payload, return the parsed JSON response.
13
-
14
- Raises RuntimeError on HTTP or network errors — providers surface those
15
- to the agent loop, which turns them into tool-result errors rather than
16
- crashing the turn.
17
- """
18
- body = json.dumps(payload).encode("utf-8")
19
- hdrs = {"content-type": "application/json", **headers}
20
- req = urllib.request.Request(url, data=body, headers=hdrs, method="POST")
21
- try:
22
- with urllib.request.urlopen(req, timeout=timeout) as resp:
23
- raw = resp.read().decode("utf-8")
24
- except urllib.error.HTTPError as exc:
25
- err_body = exc.read().decode("utf-8", errors="replace")[:500]
26
- raise RuntimeError(f"HTTP {exc.code}: {err_body}") from exc
27
- except urllib.error.URLError as exc:
28
- raise RuntimeError(f"connection error: {exc}") from exc
29
- return json.loads(raw)
30
-
31
-
32
- def get_json(url: str, headers: dict[str, str], timeout: int = TIMEOUT_SECONDS) -> dict:
33
- req = urllib.request.Request(url, headers=headers, method="GET")
34
- try:
35
- with urllib.request.urlopen(req, timeout=timeout) as resp:
36
- raw = resp.read().decode("utf-8")
37
- except urllib.error.HTTPError as exc:
38
- err_body = exc.read().decode("utf-8", errors="replace")[:500]
39
- raise RuntimeError(f"HTTP {exc.code}: {err_body}") from exc
40
- except urllib.error.URLError as exc:
41
- raise RuntimeError(f"connection error: {exc}") from exc
42
- return json.loads(raw)
1
+ """Shared HTTP helper for provider clients — stdlib urllib, no extra deps."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import ssl
7
+ import urllib.error
8
+ import urllib.request
9
+
10
+ TIMEOUT_SECONDS = 120
11
+
12
+ _SSL_CONTEXT: ssl.SSLContext | None = None
13
+
14
+
15
+ def _build_ssl_context() -> ssl.SSLContext:
16
+ insecure = os.environ.get("REIDX_INSECURE", "").strip().lower() in ("1", "true", "yes", "on")
17
+ if insecure:
18
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
19
+ ctx.check_hostname = False
20
+ ctx.verify_mode = ssl.CERT_NONE
21
+ return ctx
22
+
23
+ try:
24
+ import certifi
25
+ return ssl.create_default_context(cafile=certifi.where())
26
+ except ImportError:
27
+ pass
28
+
29
+ ctx = ssl.create_default_context()
30
+ try:
31
+ ctx.load_default_certs(ssl.Purpose.SERVER_AUTH)
32
+ except ssl.SSLError:
33
+ pass
34
+ return ctx
35
+
36
+
37
+ def _ssl_ctx() -> ssl.SSLContext:
38
+ global _SSL_CONTEXT
39
+ if _SSL_CONTEXT is None:
40
+ _SSL_CONTEXT = _build_ssl_context()
41
+ return _SSL_CONTEXT
42
+
43
+
44
+ def post_json(url: str, payload: dict, headers: dict[str, str], timeout: int = TIMEOUT_SECONDS) -> dict:
45
+ """POST a JSON payload, return the parsed JSON response.
46
+
47
+ Raises RuntimeError on HTTP or network errors — providers surface those
48
+ to the agent loop, which turns them into tool-result errors rather than
49
+ crashing the turn.
50
+ """
51
+ body = json.dumps(payload).encode("utf-8")
52
+ hdrs = {"content-type": "application/json", **headers}
53
+ req = urllib.request.Request(url, data=body, headers=hdrs, method="POST")
54
+ try:
55
+ with urllib.request.urlopen(req, timeout=timeout, context=_ssl_ctx()) as resp:
56
+ raw = resp.read().decode("utf-8")
57
+ except urllib.error.HTTPError as exc:
58
+ err_body = exc.read().decode("utf-8", errors="replace")[:500]
59
+ raise RuntimeError(f"HTTP {exc.code}: {err_body}") from exc
60
+ except urllib.error.URLError as exc:
61
+ raise RuntimeError(f"connection error: {exc}") from exc
62
+ return json.loads(raw)
63
+
64
+
65
+ def get_json(url: str, headers: dict[str, str], timeout: int = TIMEOUT_SECONDS) -> dict:
66
+ req = urllib.request.Request(url, headers=headers, method="GET")
67
+ try:
68
+ with urllib.request.urlopen(req, timeout=timeout, context=_ssl_ctx()) as resp:
69
+ raw = resp.read().decode("utf-8")
70
+ except urllib.error.HTTPError as exc:
71
+ err_body = exc.read().decode("utf-8", errors="replace")[:500]
72
+ raise RuntimeError(f"HTTP {exc.code}: {err_body}") from exc
73
+ except urllib.error.URLError as exc:
74
+ raise RuntimeError(f"connection error: {exc}") from exc
75
+ return json.loads(raw)
@@ -127,7 +127,10 @@ class ProviderStore:
127
127
  return True
128
128
 
129
129
 
130
- def validate_provider(record: ProviderRecord) -> tuple[bool, str]:
130
+ def validate_provider(
131
+ record: ProviderRecord,
132
+ skip_verify: bool = False,
133
+ ) -> tuple[bool, str]:
131
134
  try:
132
135
  provider = build_provider(record)
133
136
  except (ValueError, TypeError) as exc:
@@ -137,15 +140,22 @@ def validate_provider(record: ProviderRecord) -> tuple[bool, str]:
137
140
  return False, "API key required for this provider kind"
138
141
  if not record.api_key:
139
142
  return True, "no key required"
143
+ if skip_verify:
144
+ return True, "verification skipped"
140
145
  try:
141
146
  models = provider.fetch_models()
147
+ except RuntimeError as exc:
148
+ msg = str(exc)
149
+ if msg.startswith("HTTP 401") or msg.startswith("HTTP 403"):
150
+ return False, f"authentication failed ({msg})"
151
+ if msg.startswith("HTTP "):
152
+ return False, f"provider error: {msg}"
153
+ return False, f"{msg} (use --skip-verify to save anyway)"
142
154
  except Exception as exc:
143
- return False, f"could not connect to provider: {exc}"
155
+ return False, f"unexpected error: {exc} (use --skip-verify to save anyway)"
144
156
  if models:
145
157
  return True, f"ok ({len(models)} models available)"
146
- if record.kind == "ollama":
147
- return True, "connected"
148
- return False, "key rejected or no models returned"
158
+ return True, "connected (no models endpoint or empty list)"
149
159
 
150
160
 
151
161
  def load_into(registry: ProviderRegistry, storage_root: Path) -> list[str]:
@@ -610,8 +610,14 @@ class ProviderPalette:
610
610
  )
611
611
  ok, msg = validate_provider(record)
612
612
  if not ok:
613
- self._show_message(f"Key validation failed: {msg}", self.WIZARD)
614
- self.wizard_step_idx = len(WIZARD_STEPS) - 1
613
+ self._confirm_text = f"Key validation failed: {msg}\nSave anyway?"
614
+ self._pending_action = lambda: self._commit_wizard_provider(
615
+ name, kind, base_url, model, auth, api_key, forced_msg=msg,
616
+ )
617
+ self.screen = self.CONFIRM
618
+ self.selected_index = 0
619
+ self._scroll_offset = 0
620
+ self._invalidate()
615
621
  return
616
622
  if not model:
617
623
  try:
@@ -622,6 +628,18 @@ class ProviderPalette:
622
628
  if models:
623
629
  model = models[0]
624
630
 
631
+ self._commit_wizard_provider(name, kind, base_url, model, auth, api_key)
632
+
633
+ def _commit_wizard_provider(
634
+ self,
635
+ name: str,
636
+ kind: str,
637
+ base_url: str,
638
+ model: str,
639
+ auth: str,
640
+ api_key: str,
641
+ forced_msg: str = "",
642
+ ) -> str:
625
643
  existing = self.db.get_provider(name)
626
644
  if existing:
627
645
  existing.kind = kind
@@ -660,7 +678,9 @@ class ProviderPalette:
660
678
  self.wizard_data = {}
661
679
  self.wizard_step_idx = 0
662
680
  self.input_is_password = False
663
- self._close(f"Saved provider '{name}' ({kind})")
681
+ note = " (unverified)" if forced_msg else ""
682
+ self._close(f"Saved provider '{name}' ({kind}){note}")
683
+ return f"Saved provider '{name}'"
664
684
 
665
685
  def _do_add_key(self, label: str, api_key: str) -> None:
666
686
  if not self.current_provider:
@@ -673,13 +693,25 @@ class ProviderPalette:
673
693
  auth_method=sp.auth_method,
674
694
  )
675
695
  ok, msg = validate_provider(record)
676
- if not ok:
677
- self._show_message(f"Key validation failed: {msg}", self.MANAGE)
696
+ if ok:
697
+ self._commit_key(label, api_key, msg)
678
698
  return
699
+ self._confirm_text = f"Key validation failed: {msg}\nSave anyway?"
700
+ self._pending_action = lambda: self._commit_key(label, api_key, "saved (unverified)")
701
+ self.screen = self.CONFIRM
702
+ self.selected_index = 0
703
+ self._scroll_offset = 0
704
+ self._invalidate()
705
+
706
+ def _commit_key(self, label: str, api_key: str, msg: str) -> str:
707
+ if not self.current_provider:
708
+ return "Error: no provider"
709
+ name = self.current_provider.name
679
710
  self.db.add_key(name, label, api_key)
680
711
  self.current_provider = self.db.get_provider(name)
681
712
  self._register_current()
682
713
  self._show_message(f"Added key '{label}' ({msg})", self.MANAGE)
714
+ return f"Added key '{label}'"
683
715
 
684
716
  def _do_delete_key(self, key_id: str, label: str) -> str:
685
717
  if not self.current_provider:
@@ -536,10 +536,17 @@ def _handle_providers(orchestrator: Orchestrator) -> None:
536
536
 
537
537
 
538
538
  def _handle_connect(orchestrator: Orchestrator, arg: str) -> None:
539
- parts = arg.split()
539
+ skip_verify = False
540
+ tokens = arg.split()
541
+ parts: list[str] = []
542
+ for tok in tokens:
543
+ if tok in ("--skip-verify", "--skip", "--no-verify"):
544
+ skip_verify = True
545
+ else:
546
+ parts.append(tok)
540
547
  if len(parts) < 3:
541
548
  render.print_error(
542
- "usage: /connect <name> <kind> <base_url> [api_key] [model] "
549
+ "usage: /connect <name> <kind> <base_url> [api_key] [model] [--skip-verify] "
543
550
  f"(kind: {'|'.join(SUPPORTED_KINDS)})"
544
551
  )
545
552
  return
@@ -554,7 +561,7 @@ def _handle_connect(orchestrator: Orchestrator, arg: str) -> None:
554
561
  model = parts[4] if len(parts) > 4 else ""
555
562
  record = ProviderRecord(name=name, kind=kind, base_url=base_url, api_key=api_key, default_model=model)
556
563
  if api_key:
557
- ok, msg = validate_provider(record)
564
+ ok, msg = validate_provider(record, skip_verify=skip_verify)
558
565
  if not ok:
559
566
  render.print_error(f"key validation failed: {msg}")
560
567
  return
@@ -564,7 +571,7 @@ def _handle_connect(orchestrator: Orchestrator, arg: str) -> None:
564
571
  except ValueError as exc:
565
572
  render.print_error(f"failed to build provider: {exc}")
566
573
  return
567
- if not model and api_key:
574
+ if not model and api_key and not skip_verify:
568
575
  try:
569
576
  models = provider.fetch_models()
570
577
  except Exception: