Skip to content

Commit 0ff7b9c

Browse files
Merge pull request #136 from 73ai/cli-strengthen-types
refactor CLI: types, auth and cluster config
2 parents d61d116 + 2193ca2 commit 0ff7b9c

File tree

30 files changed

+370
-284
lines changed

30 files changed

+370
-284
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,5 @@ services/mcp/*
192192

193193
.shoreman.pid
194194
.aider*
195+
196+
.claude/*

cli/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.claude/*

cli/Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.PHONY: install lint format typecheck check test clean
2+
3+
install:
4+
uv sync
5+
6+
lint:
7+
uv run ruff check src/
8+
9+
format:
10+
uv run ruff format src/
11+
uv run ruff check --fix src/
12+
13+
typecheck:
14+
uv run pyright src/
15+
16+
check: lint typecheck
17+
18+
test:
19+
uv run pytest tests/
20+
21+
clean:
22+
rm -rf .pytest_cache .ruff_cache .mypy_cache __pycache__
23+
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
24+
find . -type f -name "*.pyc" -delete 2>/dev/null || true

cli/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,7 @@ infragpt = "infragpt.main:cli"
4343

4444
[dependency-groups]
4545
dev = [
46+
"pyright>=1.1.407",
4647
"pytest>=8.4.1",
48+
"ruff>=0.14.10",
4749
]

cli/src/infragpt/agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ def add_message(
5050
self,
5151
role: str,
5252
content: str,
53-
tool_calls: Optional[List[Dict]] = None,
53+
tool_calls: Optional[List[Dict[str, Any]]] = None,
5454
tool_call_id: Optional[str] = None,
55-
):
55+
) -> None:
5656
"""Add a message to the conversation context."""
57-
message_dict = {"role": role, "content": content}
57+
message_dict: Dict[str, Any] = {"role": role, "content": content}
5858

5959
if tool_calls:
6060
message_dict["tool_calls"] = tool_calls

cli/src/infragpt/api_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from dataclasses import dataclass
22
from typing import Optional
3+
34
import httpx
45

6+
from infragpt.config import get_api_base_url
7+
58

69
@dataclass
710
class DeviceFlowResponse:
@@ -52,8 +55,8 @@ def __init__(self, status_code: int, message: str):
5255

5356

5457
class InfraGPTClient:
55-
def __init__(self, api_base_url: str, timeout: float = 30.0):
56-
self.api_base_url = api_base_url.rstrip("/")
58+
def __init__(self, timeout: float = 30.0):
59+
self.api_base_url = get_api_base_url()
5760
self.timeout = timeout
5861

5962
def _make_request(

cli/src/infragpt/auth.py

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import httpx
1010
from cryptography.fernet import InvalidToken
1111

12-
from infragpt.config import CONFIG_DIR, console
12+
from infragpt.config import CONFIG_DIR, console, get_console_base_url
1313
from infragpt.encryption import (
1414
encrypt_data,
1515
decrypt_data,
@@ -35,20 +35,6 @@
3535
TOKEN_REFRESH_THRESHOLD_HOURS = 1
3636

3737

38-
def _get_api_base_url(api_base_url: Optional[str]) -> str:
39-
DEFAULT = "https://api.infragpt.io"
40-
if api_base_url:
41-
return api_base_url.rstrip("/")
42-
return DEFAULT
43-
44-
45-
def _get_console_base_url(console_base_url: Optional[str]) -> str:
46-
DEFAULT = "https://app.infragpt.io"
47-
if console_base_url:
48-
return console_base_url.rstrip("/")
49-
return DEFAULT
50-
51-
5238
@dataclass
5339
class AuthStatus:
5440
authenticated: bool
@@ -57,7 +43,6 @@ class AuthStatus:
5743
access_token: Optional[str] = None
5844
refresh_token: Optional[str] = None
5945
expires_at: Optional[str] = None
60-
api_base_url: Optional[str] = None
6146

6247

6348
def _load_auth_data() -> Optional[dict]:
@@ -108,7 +93,6 @@ def get_auth_status() -> AuthStatus:
10893
access_token=data.get("access_token"),
10994
refresh_token=data.get("refresh_token"),
11095
expires_at=data.get("expires_at"),
111-
api_base_url=data.get("api_base_url"),
11296
)
11397

11498

@@ -150,8 +134,7 @@ def refresh_token_if_needed() -> bool:
150134
return True
151135

152136
try:
153-
api_base_url = data.get("api_base_url")
154-
client = InfraGPTClient(api_base_url=api_base_url)
137+
client = InfraGPTClient()
155138
result = client.refresh_token(refresh_token)
156139

157140
from datetime import timedelta
@@ -171,12 +154,9 @@ def refresh_token_if_needed() -> bool:
171154
return False
172155

173156

174-
def login(
175-
api_base_url: Optional[str] = None, console_base_url: Optional[str] = None
176-
) -> None:
157+
def login() -> None:
177158
"""Authenticate with InfraGPT platform using device flow."""
178-
api_url = _get_api_base_url(api_base_url)
179-
client = InfraGPTClient(api_base_url=api_url)
159+
client = InfraGPTClient()
180160

181161
console.print("\n[bold]Authenticating with InfraGPT...[/bold]\n")
182162

@@ -190,7 +170,7 @@ def login(
190170
if flow.verification_url.startswith("http"):
191171
verification_url = flow.verification_url
192172
else:
193-
console_url = _get_console_base_url(console_base_url)
173+
console_url = get_console_base_url()
194174
path = flow.verification_url.lstrip("/")
195175
verification_url = f"{console_url}/{path}"
196176

@@ -238,7 +218,6 @@ def login(
238218
"organization_id": result.organization_id,
239219
"user_id": result.user_id,
240220
"expires_at": expires_at.isoformat(),
241-
"api_base_url": api_base_url or InfraGPTClient.DEFAULT_SERVER_URL,
242221
}
243222
_save_auth_data(auth_data)
244223

@@ -264,8 +243,7 @@ def logout() -> None:
264243

265244
if data and data.get("access_token"):
266245
try:
267-
api_base_url = data.get("api_base_url")
268-
client = InfraGPTClient(api_base_url=api_base_url)
246+
client = InfraGPTClient()
269247
client.revoke_token(data["access_token"])
270248
except (InfraGPTAPIError, httpx.RequestError):
271249
pass # Token may already be invalid; don't fail logout
@@ -285,7 +263,7 @@ def fetch_gcp_credentials() -> Optional[GCPCredentials]:
285263
return None
286264

287265
try:
288-
client = InfraGPTClient(api_base_url=status.api_base_url)
266+
client = InfraGPTClient()
289267
return client.get_gcp_credentials(status.access_token)
290268
except InfraGPTAPIError:
291269
return None
@@ -298,7 +276,7 @@ def fetch_gke_cluster_info() -> Optional[GKEClusterInfo]:
298276
return None
299277

300278
try:
301-
client = InfraGPTClient(api_base_url=status.api_base_url)
279+
client = InfraGPTClient()
302280
return client.get_gke_cluster_info(status.access_token)
303281
except InfraGPTAPIError:
304282
return None
@@ -326,7 +304,7 @@ def validate_token_with_api() -> None:
326304
raise AuthValidationError("Not authenticated")
327305

328306
try:
329-
client = InfraGPTClient(api_base_url=status.api_base_url)
307+
client = InfraGPTClient()
330308
client.validate_token(status.access_token)
331309
except InfraGPTAPIError as e:
332310
if e.status_code == 401:
@@ -365,8 +343,7 @@ def refresh_token_strict() -> None:
365343
return
366344

367345
try:
368-
api_base_url = data.get("api_base_url")
369-
client = InfraGPTClient(api_base_url=api_base_url)
346+
client = InfraGPTClient()
370347
result = client.refresh_token(refresh_token)
371348

372349
from datetime import timedelta
@@ -394,7 +371,7 @@ def fetch_gcp_credentials_strict() -> GCPCredentials:
394371
raise GCPCredentialError("Not authenticated")
395372

396373
try:
397-
client = InfraGPTClient(api_base_url=status.api_base_url)
374+
client = InfraGPTClient()
398375
return client.get_gcp_credentials(status.access_token)
399376
except InfraGPTAPIError as e:
400377
if e.status_code == 404:
@@ -413,7 +390,7 @@ def fetch_gke_cluster_info_strict() -> GKEClusterInfo:
413390
raise GKEClusterError("Not authenticated")
414391

415392
try:
416-
client = InfraGPTClient(api_base_url=status.api_base_url)
393+
client = InfraGPTClient()
417394
return client.get_gke_cluster_info(status.access_token)
418395
except InfraGPTAPIError as e:
419396
if e.status_code == 404:

cli/src/infragpt/bump_version.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from pathlib import Path
77

88

9-
def get_current_version():
9+
def get_current_version() -> str:
10+
"""Get the current version from __init__.py."""
1011
init_file = Path("src/infragpt/__init__.py")
1112
if not init_file.exists():
1213
raise FileNotFoundError(f"Could not find {init_file}")
@@ -21,8 +22,8 @@ def get_current_version():
2122
return match.group(1)
2223

2324

24-
def update_version(new_version):
25-
# Update version in __init__.py
25+
def update_version(new_version: str) -> None:
26+
"""Update version in __init__.py and pyproject.toml."""
2627
init_file = Path("src/infragpt/__init__.py")
2728
with open(init_file, "r") as f:
2829
content = f.read()
@@ -49,8 +50,8 @@ def update_version(new_version):
4950
print(f"Updated version to {new_version}")
5051

5152

52-
def commit_and_tag(version):
53-
# Commit changes
53+
def commit_and_tag(version: str) -> None:
54+
"""Commit version bump and create a git tag."""
5455
subprocess.run(
5556
["git", "add", "src/infragpt/__init__.py", "pyproject.toml"], check=True
5657
)
@@ -67,7 +68,8 @@ def commit_and_tag(version):
6768
print(f" git push origin master && git push origin {tag_name}")
6869

6970

70-
def bump_version(part="patch"):
71+
def bump_version(part: str = "patch") -> str:
72+
"""Calculate the new version based on the bump type."""
7173
current = get_current_version()
7274
major, minor, patch = map(int, current.split("."))
7375

@@ -83,7 +85,8 @@ def bump_version(part="patch"):
8385
return new_version
8486

8587

86-
def main():
88+
def main() -> None:
89+
"""Main entry point for the version bump CLI."""
8790
parser = argparse.ArgumentParser(description="Bump InfraGPT version")
8891
parser.add_argument(
8992
"part",

cli/src/infragpt/config.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import yaml
33
import pathlib
4+
from typing import Any, Dict
45

56
from rich.console import Console
67

@@ -17,7 +18,23 @@
1718
CONFIG_FILE = CONFIG_DIR / "config.yaml"
1819

1920

20-
def load_config():
21+
def is_dev_mode() -> bool:
22+
return os.environ.get("INFRAGPT_DEV_MODE", "").lower() == "true"
23+
24+
25+
def get_api_base_url() -> str:
26+
if is_dev_mode():
27+
return "http://localhost:8080"
28+
return "https://api.infragpt.io"
29+
30+
31+
def get_console_base_url() -> str:
32+
if is_dev_mode():
33+
return "http://localhost:5173"
34+
return "https://app.infragpt.io"
35+
36+
37+
def load_config() -> Dict[str, Any]:
2138
"""Load configuration from config file."""
2239
if not CONFIG_FILE.exists():
2340
return {}
@@ -30,7 +47,7 @@ def load_config():
3047
return {}
3148

3249

33-
def save_config(config):
50+
def save_config(config: Dict[str, Any]) -> None:
3451
"""Save configuration to config file."""
3552
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
3653

@@ -41,7 +58,7 @@ def save_config(config):
4158
console.print(f"[yellow]Warning:[/yellow] Could not save config: {e}")
4259

4360

44-
def init_config():
61+
def init_config() -> None:
4562
"""Initialize configuration file with environment variables if it doesn't exist."""
4663
if CONFIG_FILE.exists():
4764
return
@@ -52,24 +69,17 @@ def init_config():
5269

5370
init_history_dir()
5471

55-
config = {}
56-
57-
from infragpt.llm import validate_env_api_keys
72+
config: Dict[str, Any] = {}
5873

5974
openai_key = os.getenv("OPENAI_API_KEY")
6075
anthropic_key = os.getenv("ANTHROPIC_API_KEY")
6176
env_model = os.getenv("INFRAGPT_MODEL")
6277

63-
model, api_key = validate_env_api_keys()
64-
65-
if model and api_key:
66-
config["model"] = model
67-
config["api_key"] = api_key
68-
elif anthropic_key and (not env_model or env_model == "claude"):
69-
config["model"] = "claude"
78+
if anthropic_key and (not env_model or env_model == "claude"):
79+
config["model"] = "anthropic:claude-sonnet-4-20250514"
7080
config["api_key"] = anthropic_key
7181
elif openai_key and (not env_model or env_model == "gpt4o"):
72-
config["model"] = "gpt4o"
82+
config["model"] = "openai:gpt-4o"
7383
config["api_key"] = openai_key
7484

7585
if config:

0 commit comments

Comments
 (0)