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.
@@ -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
+