@agxnte/reidx 2.0.4 → 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.4",
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.4"
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.4"
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)
@@ -160,11 +160,7 @@ class AnthropicProvider(BaseProvider):
160
160
 
161
161
  def fetch_models(self) -> list[str]:
162
162
  url = f"{self.base_url}/v1/models"
163
- try:
164
- body = get_json(url, self._headers())
165
- except RuntimeError:
166
- log.debug("failed to fetch models from %s", url)
167
- return []
163
+ body = get_json(url, self._headers())
168
164
  models: list[str] = []
169
165
  for item in body.get("data", []):
170
166
  mid = item.get("id", "")
@@ -109,11 +109,7 @@ class OllamaProvider(BaseProvider):
109
109
  )
110
110
 
111
111
  def fetch_models(self) -> list[str]:
112
- try:
113
- body = get_json(f"{self.base_url}/api/tags", {})
114
- except RuntimeError:
115
- log.debug("failed to fetch models from ollama")
116
- return []
112
+ body = get_json(f"{self.base_url}/api/tags", {})
117
113
  models: list[str] = []
118
114
  for item in body.get("models", []):
119
115
  name = item.get("name", "")
@@ -127,11 +127,7 @@ class OpenAIProvider(BaseProvider):
127
127
 
128
128
  def fetch_models(self) -> list[str]:
129
129
  url = f"{self.base_url}/v1/models"
130
- try:
131
- body = get_json(url, self._headers())
132
- except RuntimeError:
133
- log.debug("failed to fetch models from %s", url)
134
- return []
130
+ body = get_json(url, self._headers())
135
131
  models: list[str] = []
136
132
  for item in body.get("data", []):
137
133
  mid = item.get("id", "")
@@ -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()
142
- except Exception:
143
- return False, "could not connect to provider"
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)"
154
+ except Exception as 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]:
@@ -606,11 +606,18 @@ class ProviderPalette:
606
606
  record = ProviderRecord(
607
607
  name=name, kind=kind, base_url=base_url,
608
608
  api_key=api_key, default_model=model,
609
+ auth_method=auth,
609
610
  )
610
611
  ok, msg = validate_provider(record)
611
612
  if not ok:
612
- self._show_message(f"Key validation failed: {msg}", self.WIZARD)
613
- 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()
614
621
  return
615
622
  if not model:
616
623
  try:
@@ -621,6 +628,18 @@ class ProviderPalette:
621
628
  if models:
622
629
  model = models[0]
623
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:
624
643
  existing = self.db.get_provider(name)
625
644
  if existing:
626
645
  existing.kind = kind
@@ -659,7 +678,9 @@ class ProviderPalette:
659
678
  self.wizard_data = {}
660
679
  self.wizard_step_idx = 0
661
680
  self.input_is_password = False
662
- 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}'"
663
684
 
664
685
  def _do_add_key(self, label: str, api_key: str) -> None:
665
686
  if not self.current_provider:
@@ -669,15 +690,28 @@ class ProviderPalette:
669
690
  record = ProviderRecord(
670
691
  name=name, kind=sp.kind, base_url=sp.base_url,
671
692
  api_key=api_key, default_model=sp.default_model,
693
+ auth_method=sp.auth_method,
672
694
  )
673
695
  ok, msg = validate_provider(record)
674
- if not ok:
675
- self._show_message(f"Key validation failed: {msg}", self.MANAGE)
696
+ if ok:
697
+ self._commit_key(label, api_key, msg)
676
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
677
710
  self.db.add_key(name, label, api_key)
678
711
  self.current_provider = self.db.get_provider(name)
679
712
  self._register_current()
680
713
  self._show_message(f"Added key '{label}' ({msg})", self.MANAGE)
714
+ return f"Added key '{label}'"
681
715
 
682
716
  def _do_delete_key(self, key_id: str, label: str) -> str:
683
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: