entelligence-cli 0.1.0__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.
- entelligence_cli-0.1.0/PKG-INFO +150 -0
- entelligence_cli-0.1.0/README.md +120 -0
- entelligence_cli-0.1.0/entelligence_cli/__init__.py +10 -0
- entelligence_cli-0.1.0/entelligence_cli/api_client.py +350 -0
- entelligence_cli-0.1.0/entelligence_cli/cli.py +785 -0
- entelligence_cli-0.1.0/entelligence_cli/config.py +112 -0
- entelligence_cli-0.1.0/entelligence_cli/git_operations.py +280 -0
- entelligence_cli-0.1.0/entelligence_cli/terminal_ui.py +852 -0
- entelligence_cli-0.1.0/entelligence_cli/textual_ui.py +98 -0
- entelligence_cli-0.1.0/entelligence_cli.egg-info/PKG-INFO +150 -0
- entelligence_cli-0.1.0/entelligence_cli.egg-info/SOURCES.txt +15 -0
- entelligence_cli-0.1.0/entelligence_cli.egg-info/dependency_links.txt +1 -0
- entelligence_cli-0.1.0/entelligence_cli.egg-info/entry_points.txt +2 -0
- entelligence_cli-0.1.0/entelligence_cli.egg-info/requires.txt +9 -0
- entelligence_cli-0.1.0/entelligence_cli.egg-info/top_level.txt +1 -0
- entelligence_cli-0.1.0/pyproject.toml +62 -0
- entelligence_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entelligence-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: EntelligenceAI CLI - AI-powered code review from your terminal
|
|
5
|
+
Author-email: EntelligenceAI <info@entelligence.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://entelligenceai.com
|
|
8
|
+
Project-URL: Documentation, https://github.com/entelligenceai/cli#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/entelligenceai/cli
|
|
10
|
+
Keywords: code-review,ai,cli,git,entelligence
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: click>=8.0.0
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: flake8>=5.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=0.990; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# EntelligenceAI CLI
|
|
32
|
+
|
|
33
|
+
AI-powered code review from your terminal.
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Setup (this will install uv if not already there)
|
|
39
|
+
make setup
|
|
40
|
+
|
|
41
|
+
# Authenticate
|
|
42
|
+
entelligence auth login
|
|
43
|
+
|
|
44
|
+
# Run review
|
|
45
|
+
entelligence review
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Development (recommended)
|
|
52
|
+
make setup
|
|
53
|
+
# or
|
|
54
|
+
uv sync --dev
|
|
55
|
+
|
|
56
|
+
# Production
|
|
57
|
+
uv pip install entelligence-cli
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Authentication
|
|
61
|
+
|
|
62
|
+
Get your API key from [app.entelligence.ai/settings?tab=api](https://app.entelligence.ai/settings?tab=api)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
entelligence auth login
|
|
66
|
+
entelligence auth status
|
|
67
|
+
entelligence auth logout
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Review changes
|
|
74
|
+
entelligence review
|
|
75
|
+
|
|
76
|
+
# Options
|
|
77
|
+
entelligence review --base-branch main --priority high --mode concise
|
|
78
|
+
entelligence review --include-uncommitted # Include uncommitted changes
|
|
79
|
+
entelligence review --plain # Plain text output
|
|
80
|
+
entelligence review --debug # Debug mode
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
**Config file:** `~/.entelligence/config.json` (permissions: `0o600`)
|
|
86
|
+
|
|
87
|
+
**Stored locally:**
|
|
88
|
+
- `api_key` - Saved during `entelligence auth login`
|
|
89
|
+
|
|
90
|
+
**Fetched from backend:**
|
|
91
|
+
- `OrgUUID` - Fetched from `/getUserInfo/` endpoint at session start (required)
|
|
92
|
+
- `GitHubToken` - Fetched from `/getUserInfo/` endpoint at session start (optional)
|
|
93
|
+
|
|
94
|
+
**Environment variables:**
|
|
95
|
+
- `ENTELLIGENCE_API_KEY` - API key (auto-saved to config file on login)
|
|
96
|
+
- `ENTELLIGENCE_ENDPOINT` - API endpoint (default: `http://127.0.0.1:8000/generateReviewForPR/`)
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
make setup # Initial setup
|
|
102
|
+
make test # Run tests
|
|
103
|
+
make format # Format code
|
|
104
|
+
make lint # Lint code
|
|
105
|
+
make type-check # Type check
|
|
106
|
+
make check # Run all checks
|
|
107
|
+
make clean # Clean build artifacts
|
|
108
|
+
make help # Show all commands
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Publishing
|
|
112
|
+
|
|
113
|
+
The package is automatically published to PyPI when a new GitHub release is created.
|
|
114
|
+
|
|
115
|
+
**Before publishing:**
|
|
116
|
+
1. Update version in `pyproject.toml`
|
|
117
|
+
2. Commit and push changes
|
|
118
|
+
3. Create a new GitHub release (tagged with version, e.g., `v0.1.0`)
|
|
119
|
+
|
|
120
|
+
**Manual publishing:**
|
|
121
|
+
```bash
|
|
122
|
+
# Build package
|
|
123
|
+
uv build
|
|
124
|
+
|
|
125
|
+
# Publish to PyPI (requires PyPI credentials)
|
|
126
|
+
uv publish
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Setup PyPI Trusted Publisher (one-time):**
|
|
130
|
+
1. Go to PyPI project settings → "Publishing" → "Add a new trusted publisher"
|
|
131
|
+
2. Select "GitHub" as publisher
|
|
132
|
+
3. Specify repository: `Entelligence-AI/cli`
|
|
133
|
+
4. Workflow file: `.github/workflows/publish.yml`
|
|
134
|
+
5. Environment name: `(Any)` or `pypi`
|
|
135
|
+
|
|
136
|
+
## Uninstall
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Uninstall the CLI tool
|
|
140
|
+
uv pip uninstall entelligence-cli
|
|
141
|
+
|
|
142
|
+
# Remove configuration files (API keys, tokens, etc.)
|
|
143
|
+
rm -rf ~/.entelligence/
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Troubleshooting
|
|
147
|
+
|
|
148
|
+
**401 Unauthorized:** Verify API key with `entelligence auth status`
|
|
149
|
+
|
|
150
|
+
**Connection errors:** Ensure backend is running at the configured endpoint
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# EntelligenceAI CLI
|
|
2
|
+
|
|
3
|
+
AI-powered code review from your terminal.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Setup (this will install uv if not already there)
|
|
9
|
+
make setup
|
|
10
|
+
|
|
11
|
+
# Authenticate
|
|
12
|
+
entelligence auth login
|
|
13
|
+
|
|
14
|
+
# Run review
|
|
15
|
+
entelligence review
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Development (recommended)
|
|
22
|
+
make setup
|
|
23
|
+
# or
|
|
24
|
+
uv sync --dev
|
|
25
|
+
|
|
26
|
+
# Production
|
|
27
|
+
uv pip install entelligence-cli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Authentication
|
|
31
|
+
|
|
32
|
+
Get your API key from [app.entelligence.ai/settings?tab=api](https://app.entelligence.ai/settings?tab=api)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
entelligence auth login
|
|
36
|
+
entelligence auth status
|
|
37
|
+
entelligence auth logout
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Review changes
|
|
44
|
+
entelligence review
|
|
45
|
+
|
|
46
|
+
# Options
|
|
47
|
+
entelligence review --base-branch main --priority high --mode concise
|
|
48
|
+
entelligence review --include-uncommitted # Include uncommitted changes
|
|
49
|
+
entelligence review --plain # Plain text output
|
|
50
|
+
entelligence review --debug # Debug mode
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
**Config file:** `~/.entelligence/config.json` (permissions: `0o600`)
|
|
56
|
+
|
|
57
|
+
**Stored locally:**
|
|
58
|
+
- `api_key` - Saved during `entelligence auth login`
|
|
59
|
+
|
|
60
|
+
**Fetched from backend:**
|
|
61
|
+
- `OrgUUID` - Fetched from `/getUserInfo/` endpoint at session start (required)
|
|
62
|
+
- `GitHubToken` - Fetched from `/getUserInfo/` endpoint at session start (optional)
|
|
63
|
+
|
|
64
|
+
**Environment variables:**
|
|
65
|
+
- `ENTELLIGENCE_API_KEY` - API key (auto-saved to config file on login)
|
|
66
|
+
- `ENTELLIGENCE_ENDPOINT` - API endpoint (default: `http://127.0.0.1:8000/generateReviewForPR/`)
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
make setup # Initial setup
|
|
72
|
+
make test # Run tests
|
|
73
|
+
make format # Format code
|
|
74
|
+
make lint # Lint code
|
|
75
|
+
make type-check # Type check
|
|
76
|
+
make check # Run all checks
|
|
77
|
+
make clean # Clean build artifacts
|
|
78
|
+
make help # Show all commands
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Publishing
|
|
82
|
+
|
|
83
|
+
The package is automatically published to PyPI when a new GitHub release is created.
|
|
84
|
+
|
|
85
|
+
**Before publishing:**
|
|
86
|
+
1. Update version in `pyproject.toml`
|
|
87
|
+
2. Commit and push changes
|
|
88
|
+
3. Create a new GitHub release (tagged with version, e.g., `v0.1.0`)
|
|
89
|
+
|
|
90
|
+
**Manual publishing:**
|
|
91
|
+
```bash
|
|
92
|
+
# Build package
|
|
93
|
+
uv build
|
|
94
|
+
|
|
95
|
+
# Publish to PyPI (requires PyPI credentials)
|
|
96
|
+
uv publish
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Setup PyPI Trusted Publisher (one-time):**
|
|
100
|
+
1. Go to PyPI project settings → "Publishing" → "Add a new trusted publisher"
|
|
101
|
+
2. Select "GitHub" as publisher
|
|
102
|
+
3. Specify repository: `Entelligence-AI/cli`
|
|
103
|
+
4. Workflow file: `.github/workflows/publish.yml`
|
|
104
|
+
5. Environment name: `(Any)` or `pypi`
|
|
105
|
+
|
|
106
|
+
## Uninstall
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Uninstall the CLI tool
|
|
110
|
+
uv pip uninstall entelligence-cli
|
|
111
|
+
|
|
112
|
+
# Remove configuration files (API keys, tokens, etc.)
|
|
113
|
+
rm -rf ~/.entelligence/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Troubleshooting
|
|
117
|
+
|
|
118
|
+
**401 Unauthorized:** Verify API key with `entelligence auth status`
|
|
119
|
+
|
|
120
|
+
**Connection errors:** Ensure backend is running at the configured endpoint
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""EntelligenceAI CLI - AI-powered code review from your terminal."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .cli import cli
|
|
6
|
+
from .git_operations import GitOperations
|
|
7
|
+
from .api_client import APIClient, APIConfig
|
|
8
|
+
from .config import ConfigManager
|
|
9
|
+
|
|
10
|
+
__all__ = ["cli", "GitOperations", "APIClient", "APIConfig", "ConfigManager"]
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, Optional, List
|
|
4
|
+
import json
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class APIConfig:
|
|
10
|
+
endpoint: str
|
|
11
|
+
api_token: Optional[str] = None
|
|
12
|
+
timeout: int = 300 # Increase default timeout to 5 minutes for longer reviews
|
|
13
|
+
|
|
14
|
+
class APIClient:
|
|
15
|
+
def __init__(self, config: APIConfig):
|
|
16
|
+
self.config = config
|
|
17
|
+
|
|
18
|
+
def generate_review(self, payload: Dict) -> Dict:
|
|
19
|
+
"""Send code changes to EntelligenceAI for review."""
|
|
20
|
+
def _mask(tok: Optional[str]) -> str:
|
|
21
|
+
if not tok:
|
|
22
|
+
return "(empty)"
|
|
23
|
+
if len(tok) <= 8:
|
|
24
|
+
return "*" * (len(tok) - 1) + tok[-1]
|
|
25
|
+
return f"{tok[:4]}...{tok[-4:]}"
|
|
26
|
+
# Normalize API token
|
|
27
|
+
api_token = (self.config.api_token or "").strip()
|
|
28
|
+
headers = {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if api_token:
|
|
33
|
+
headers["Authorization"] = f"Bearer {api_token}"
|
|
34
|
+
logging.info(f"Using API key (masked): {_mask(api_token)}")
|
|
35
|
+
else:
|
|
36
|
+
logging.info("No API key provided; sending request without Authorization header.")
|
|
37
|
+
# Log presence of GitHub token in payload
|
|
38
|
+
gh_tok = ""
|
|
39
|
+
try:
|
|
40
|
+
gh_tok = (payload.get("githubToken") or "").strip()
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
if gh_tok:
|
|
44
|
+
logging.info(f"Including githubToken in payload (masked): {_mask(gh_tok)}")
|
|
45
|
+
else:
|
|
46
|
+
logging.info("githubToken not set in payload or empty.")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
logging.info(f"Sending request to {self.config.endpoint} (timeout {self.config.timeout}s)")
|
|
50
|
+
response = requests.post(
|
|
51
|
+
self.config.endpoint,
|
|
52
|
+
json=payload,
|
|
53
|
+
headers=headers,
|
|
54
|
+
timeout=self.config.timeout
|
|
55
|
+
)
|
|
56
|
+
logging.info(f"HTTP {response.status_code}")
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
return response.json()
|
|
59
|
+
|
|
60
|
+
except requests.exceptions.Timeout:
|
|
61
|
+
return {"error": "Request timed out - review is taking longer than expected"}
|
|
62
|
+
except requests.exceptions.HTTPError as e:
|
|
63
|
+
return {"error": f"API error: {e.response.status_code} - {e.response.text}"}
|
|
64
|
+
except requests.exceptions.RequestException as e:
|
|
65
|
+
return {"error": f"Request failed: {str(e)}"}
|
|
66
|
+
|
|
67
|
+
def _coerce_to_dict(self, resp: Any) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Try very hard to coerce backend response into a dict.
|
|
70
|
+
Handles: dict, bytes, JSON string, double-encoded JSON string,
|
|
71
|
+
and string with leading text before the first '{'.
|
|
72
|
+
"""
|
|
73
|
+
# Unwrap bytes
|
|
74
|
+
if isinstance(resp, (bytes, bytearray)):
|
|
75
|
+
try:
|
|
76
|
+
resp = resp.decode("utf-8", errors="ignore")
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
# Attempt up to 3 JSON parses (double-encoded cases)
|
|
80
|
+
for _ in range(3):
|
|
81
|
+
if isinstance(resp, dict):
|
|
82
|
+
return resp
|
|
83
|
+
if isinstance(resp, str):
|
|
84
|
+
s = resp.strip()
|
|
85
|
+
# If string has leading text, try to extract the first JSON object
|
|
86
|
+
if not (s.startswith("{") or s.startswith("[")):
|
|
87
|
+
start = s.find("{")
|
|
88
|
+
end = s.rfind("}")
|
|
89
|
+
if start != -1 and end != -1 and end > start:
|
|
90
|
+
s = s[start : end + 1]
|
|
91
|
+
try:
|
|
92
|
+
resp = json.loads(s)
|
|
93
|
+
continue
|
|
94
|
+
except Exception:
|
|
95
|
+
break
|
|
96
|
+
else:
|
|
97
|
+
break
|
|
98
|
+
# Final fallback
|
|
99
|
+
if isinstance(resp, dict):
|
|
100
|
+
return resp
|
|
101
|
+
return {"raw_response": resp, "error": "Non-JSON or unexpected backend response"}
|
|
102
|
+
|
|
103
|
+
def parse_review_response(self, response: Any) -> Dict:
|
|
104
|
+
"""Parse EntelligenceAI response into display format."""
|
|
105
|
+
# Normalize to a dict (supports double-encoded string cases)
|
|
106
|
+
response = self._coerce_to_dict(response)
|
|
107
|
+
if not isinstance(response, dict):
|
|
108
|
+
return {"error": "Unexpected backend response type", "raw_response": response}
|
|
109
|
+
if "error" in response and response["error"]:
|
|
110
|
+
return response
|
|
111
|
+
|
|
112
|
+
# Normalize multiple possible response shapes
|
|
113
|
+
comments: list[Dict] = []
|
|
114
|
+
meta: Dict = {}
|
|
115
|
+
# Capture helpful meta so we can show it even without comments
|
|
116
|
+
for key in ("releaseNote", "walkthrough_and_changes", "file_overview", "pr_diff"):
|
|
117
|
+
if key in response:
|
|
118
|
+
meta[key] = response[key]
|
|
119
|
+
|
|
120
|
+
# v1 format: {"review": {"files": [ {path, comments:[...]}, ... ] }, "security_findings": [...]}
|
|
121
|
+
if isinstance(response, dict) and "review" in response:
|
|
122
|
+
review_data = response["review"]
|
|
123
|
+
|
|
124
|
+
# Parse file-level comments
|
|
125
|
+
if "files" in review_data:
|
|
126
|
+
for file_review in review_data["files"]:
|
|
127
|
+
file_path = file_review.get("path", "unknown")
|
|
128
|
+
|
|
129
|
+
if "comments" in file_review:
|
|
130
|
+
for comment in file_review["comments"]:
|
|
131
|
+
comments.append({
|
|
132
|
+
"severity": comment.get("severity", "info"),
|
|
133
|
+
"file": file_path,
|
|
134
|
+
"line": comment.get("line"),
|
|
135
|
+
"message": comment.get("message", ""),
|
|
136
|
+
"code_snippet": comment.get("snippet", ""),
|
|
137
|
+
"language": self._detect_language(file_path)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
# Parse security findings
|
|
141
|
+
if "security_findings" in review_data:
|
|
142
|
+
for finding in review_data["security_findings"]:
|
|
143
|
+
comments.append({
|
|
144
|
+
"severity": "error",
|
|
145
|
+
"file": finding.get("file", "unknown"),
|
|
146
|
+
"line": finding.get("line"),
|
|
147
|
+
"message": f"Security: {finding.get('description', '')}",
|
|
148
|
+
"code_snippet": finding.get("snippet", ""),
|
|
149
|
+
"language": self._detect_language(finding.get("file", ""))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
# v2 format A: {"gitdiff_chunks_review": [ { "file"|"file_path"|"path": str, "comments":[...] }, ... ]}
|
|
153
|
+
# v2 format B: {"gitdiff_chunks_review": [ { "file_name"/"path": str, "body"/"bug_description": str, ... }, ... ]}
|
|
154
|
+
if isinstance(response, dict) and isinstance(response.get("gitdiff_chunks_review"), list):
|
|
155
|
+
for item in response["gitdiff_chunks_review"]:
|
|
156
|
+
# Case A: per-file bucket with comments array
|
|
157
|
+
if isinstance(item, dict) and isinstance(item.get("comments"), list):
|
|
158
|
+
file_path = item.get("file") or item.get("file_path") or item.get("path") or "unknown"
|
|
159
|
+
for c in item["comments"]:
|
|
160
|
+
comments.append({
|
|
161
|
+
"severity": c.get("severity", "info"),
|
|
162
|
+
"file": file_path,
|
|
163
|
+
"line": c.get("line") or c.get("lineno"),
|
|
164
|
+
"message": c.get("message") or c.get("text") or "",
|
|
165
|
+
"code_snippet": c.get("code_snippet") or c.get("snippet") or "",
|
|
166
|
+
"language": self._detect_language(file_path),
|
|
167
|
+
})
|
|
168
|
+
continue
|
|
169
|
+
# Case B: each element is a single review object
|
|
170
|
+
if isinstance(item, dict):
|
|
171
|
+
file_path = item.get("path") or item.get("file_name") or item.get("file") or "unknown"
|
|
172
|
+
message = item.get("body") or item.get("bug_description") or ""
|
|
173
|
+
line = item.get("line") or item.get("start_line") or None
|
|
174
|
+
# Choose best snippet field (committable suggestion)
|
|
175
|
+
snippet = (
|
|
176
|
+
item.get("commitable_suggestion") # common variant from backend
|
|
177
|
+
or item.get("committable_suggestion")
|
|
178
|
+
or item.get("committable_code")
|
|
179
|
+
or item.get("commitable_code")
|
|
180
|
+
or item.get("code_snippet")
|
|
181
|
+
or ""
|
|
182
|
+
)
|
|
183
|
+
# Strip triple-fence if present for display/application
|
|
184
|
+
if isinstance(snippet, str) and "```" in snippet:
|
|
185
|
+
snippet = "\n".join(
|
|
186
|
+
ln for ln in snippet.splitlines() if not ln.strip().startswith("```")
|
|
187
|
+
).strip()
|
|
188
|
+
suggested_code = item.get("suggested_code") or ""
|
|
189
|
+
# If suggested_code is a ```diff fenced block, strip the fence and keep as suggested_patch
|
|
190
|
+
suggested_patch = None
|
|
191
|
+
if isinstance(suggested_code, str) and "```" in suggested_code:
|
|
192
|
+
sc = suggested_code.strip()
|
|
193
|
+
if sc.startswith("```"):
|
|
194
|
+
sc = sc.strip("`")
|
|
195
|
+
# After stripping backticks, it might still include 'diff\n'
|
|
196
|
+
# Simpler: just remove surrounding triple backticks lines
|
|
197
|
+
lines = [ln for ln in suggested_code.splitlines() if not ln.strip().startswith("```")]
|
|
198
|
+
suggested_patch = "\n".join(lines).strip()
|
|
199
|
+
# Determine severity from fields
|
|
200
|
+
suggestion_type = item.get("suggestion_type") or ""
|
|
201
|
+
impact = (item.get("impact") or "").lower()
|
|
202
|
+
sev = "info"
|
|
203
|
+
if suggestion_type.lower() in ("bug fix", "bug", "error") or impact == "high":
|
|
204
|
+
sev = "error"
|
|
205
|
+
elif impact in ("medium", "med"):
|
|
206
|
+
sev = "warning"
|
|
207
|
+
else:
|
|
208
|
+
sev = "suggestion"
|
|
209
|
+
# Parse range "line_numbers": "16-26"
|
|
210
|
+
apply_start = item.get("start_line")
|
|
211
|
+
apply_end = item.get("end_line") or item.get("line")
|
|
212
|
+
ln_range = item.get("line_numbers")
|
|
213
|
+
if (apply_start is None or apply_end is None) and isinstance(ln_range, str) and "-" in ln_range:
|
|
214
|
+
try:
|
|
215
|
+
a, b = ln_range.split("-", 1)
|
|
216
|
+
apply_start = apply_start or int(a.strip())
|
|
217
|
+
apply_end = apply_end or int(b.strip())
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
# Capture optional agent prompt to display/copy
|
|
221
|
+
agent_prompt_obj = item.get("prompt_for_ai_agents_for_addressing_review")
|
|
222
|
+
agent_prompt_str = None
|
|
223
|
+
if agent_prompt_obj is not None:
|
|
224
|
+
try:
|
|
225
|
+
agent_prompt_str = json.dumps(agent_prompt_obj, indent=2)
|
|
226
|
+
except Exception:
|
|
227
|
+
agent_prompt_str = str(agent_prompt_obj)
|
|
228
|
+
extra = {
|
|
229
|
+
"suggestion_type": item.get("suggestion_type"),
|
|
230
|
+
"impact": item.get("impact"),
|
|
231
|
+
"score": item.get("score"),
|
|
232
|
+
"reasoning": item.get("reasoning"),
|
|
233
|
+
"line_numbers": item.get("line_numbers"),
|
|
234
|
+
"agent_prompt": agent_prompt_str,
|
|
235
|
+
}
|
|
236
|
+
comments.append({
|
|
237
|
+
"severity": sev,
|
|
238
|
+
"file": file_path,
|
|
239
|
+
"line": line,
|
|
240
|
+
"message": message,
|
|
241
|
+
"code_snippet": snippet,
|
|
242
|
+
"language": self._detect_language(file_path),
|
|
243
|
+
"suggested_patch": suggested_patch,
|
|
244
|
+
"apply_snippet": snippet,
|
|
245
|
+
"apply_start": apply_start,
|
|
246
|
+
"apply_end": apply_end,
|
|
247
|
+
"ai_prompt": agent_prompt_str,
|
|
248
|
+
"extra": extra,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
# Build summary
|
|
252
|
+
summary = {
|
|
253
|
+
"files_changed": response.get("files_changed", 0) or len(meta.get("file_overview", {}).get("files_selected", [])),
|
|
254
|
+
"errors": len([c for c in comments if c["severity"] == "error"]),
|
|
255
|
+
"warnings": len([c for c in comments if c["severity"] == "warning"]),
|
|
256
|
+
"suggestions": len([c for c in comments if c["severity"] in ["info", "suggestion"]])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"comments": comments,
|
|
261
|
+
"summary": summary,
|
|
262
|
+
"meta": meta,
|
|
263
|
+
"raw_response": response # Keep raw response for debugging
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
def get_user_info(self) -> Optional[Dict[str, Any]]:
|
|
267
|
+
"""Fetch user information from backend including org UUID, GitHub token, etc."""
|
|
268
|
+
if not self.config.api_token:
|
|
269
|
+
logging.warning("No API token available to fetch user info")
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
# Extract base URL from endpoint (remove /generateReviewForPR/)
|
|
273
|
+
base_url = self.config.endpoint.rsplit('/', 2)[0] if '/' in self.config.endpoint else self.config.endpoint
|
|
274
|
+
user_info_url = f"{base_url}/getUserInfo/"
|
|
275
|
+
|
|
276
|
+
headers = {
|
|
277
|
+
"Authorization": f"Bearer {self.config.api_token}",
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
logging.info(f"Fetching user info from {user_info_url}")
|
|
282
|
+
response = requests.get(
|
|
283
|
+
user_info_url,
|
|
284
|
+
headers=headers,
|
|
285
|
+
timeout=10
|
|
286
|
+
)
|
|
287
|
+
response.raise_for_status()
|
|
288
|
+
data = response.json()
|
|
289
|
+
|
|
290
|
+
# Handle potential double-stringified JSON
|
|
291
|
+
if isinstance(data, str):
|
|
292
|
+
try:
|
|
293
|
+
data = json.loads(data)
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
if isinstance(data, dict):
|
|
298
|
+
if data.get("Error") or data.get("error"):
|
|
299
|
+
error_msg = data.get("Error") or data.get("error")
|
|
300
|
+
logging.warning(f"Error fetching user info: {error_msg}")
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
# Return the user info dict with expected fields
|
|
304
|
+
return {
|
|
305
|
+
"UserUUID": data.get("UserUUID"),
|
|
306
|
+
"OrgUUID": data.get("OrgUUID"),
|
|
307
|
+
"OrgName": data.get("OrgName"),
|
|
308
|
+
"Email": data.get("Email"),
|
|
309
|
+
"Name": data.get("Name"),
|
|
310
|
+
"GitHubToken": data.get("GitHubToken"),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return None
|
|
314
|
+
except requests.exceptions.HTTPError as e:
|
|
315
|
+
# Silently handle 404 (endpoint doesn't exist yet on older backends)
|
|
316
|
+
if e.response.status_code == 404:
|
|
317
|
+
logging.debug(f"getUserInfo endpoint not available (404) - using fallback")
|
|
318
|
+
return None
|
|
319
|
+
logging.warning(f"Failed to fetch user info: {str(e)}")
|
|
320
|
+
return None
|
|
321
|
+
except requests.exceptions.RequestException as e:
|
|
322
|
+
logging.debug(f"Failed to fetch user info: {str(e)}")
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
def _detect_language(self, file_path: str) -> str:
|
|
326
|
+
"""Detect programming language from file extension."""
|
|
327
|
+
ext_map = {
|
|
328
|
+
".py": "python",
|
|
329
|
+
".js": "javascript",
|
|
330
|
+
".ts": "typescript",
|
|
331
|
+
".jsx": "javascript",
|
|
332
|
+
".tsx": "typescript",
|
|
333
|
+
".java": "java",
|
|
334
|
+
".go": "go",
|
|
335
|
+
".rs": "rust",
|
|
336
|
+
".cpp": "cpp",
|
|
337
|
+
".c": "c",
|
|
338
|
+
".rb": "ruby",
|
|
339
|
+
".php": "php",
|
|
340
|
+
".swift": "swift",
|
|
341
|
+
".kt": "kotlin",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for ext, lang in ext_map.items():
|
|
345
|
+
if file_path.endswith(ext):
|
|
346
|
+
return lang
|
|
347
|
+
|
|
348
|
+
return "text"
|
|
349
|
+
|
|
350
|
+
|