janito 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- janito/__init__.py +48 -1
- janito/__main__.py +29 -334
- janito/agents/__init__.py +22 -0
- janito/agents/agent.py +21 -0
- janito/{claude.py → agents/claudeai.py} +10 -5
- janito/agents/openai.py +53 -0
- janito/agents/test.py +34 -0
- janito/analysis/__init__.py +33 -0
- janito/analysis/display.py +149 -0
- janito/analysis/options.py +112 -0
- janito/analysis/prompts.py +75 -0
- janito/change/__init__.py +19 -0
- janito/change/applier.py +269 -0
- janito/{contentchange.py → change/content.py} +5 -27
- janito/change/indentation.py +33 -0
- janito/change/position.py +169 -0
- janito/changehistory.py +46 -0
- janito/changeviewer/__init__.py +12 -0
- janito/changeviewer/diff.py +28 -0
- janito/changeviewer/panels.py +268 -0
- janito/changeviewer/styling.py +59 -0
- janito/changeviewer/themes.py +57 -0
- janito/cli/__init__.py +2 -0
- janito/cli/commands.py +53 -0
- janito/cli/functions.py +286 -0
- janito/cli/registry.py +26 -0
- janito/common.py +9 -9
- janito/console/__init__.py +3 -0
- janito/console/commands.py +112 -0
- janito/console/core.py +62 -0
- janito/console/display.py +157 -0
- janito/fileparser.py +292 -83
- janito/prompts.py +21 -6
- janito/qa.py +7 -5
- janito/review.py +13 -0
- janito/scan.py +44 -5
- janito/tests/test_fileparser.py +26 -0
- janito-0.5.0.dist-info/METADATA +146 -0
- janito-0.5.0.dist-info/RECORD +45 -0
- janito/analysis.py +0 -281
- janito/changeapplier.py +0 -436
- janito/changeviewer.py +0 -350
- janito/console.py +0 -330
- janito-0.4.0.dist-info/METADATA +0 -164
- janito-0.4.0.dist-info/RECORD +0 -21
- /janito/{contextparser.py → _contextparser.py} +0 -0
- {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/WHEEL +0 -0
- {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/entry_points.txt +0 -0
- {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/licenses/LICENSE +0 -0
janito/review.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.markdown import Markdown
|
3
|
+
from janito.common import progress_send_message
|
4
|
+
from janito.agents import AIAgent
|
5
|
+
|
6
|
+
def review_text(text: str, raw: bool = False) -> None:
|
7
|
+
"""Review the provided text using Claude"""
|
8
|
+
console = Console()
|
9
|
+
response = progress_send_message(f"Please review this text and provide feedback:\n\n{text}")
|
10
|
+
if raw:
|
11
|
+
console.print(response)
|
12
|
+
else:
|
13
|
+
console.print(Markdown(response))
|
janito/scan.py
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
from pathlib import Path
|
2
|
-
from typing import List, Tuple
|
2
|
+
from typing import List, Tuple, Set
|
3
3
|
from rich.console import Console
|
4
4
|
from rich.columns import Columns
|
5
|
+
from rich.panel import Panel
|
5
6
|
from janito.config import config
|
6
7
|
from pathspec import PathSpec
|
7
8
|
from pathspec.patterns import GitWildMatchPattern
|
9
|
+
from collections import defaultdict
|
10
|
+
|
8
11
|
|
9
12
|
|
10
13
|
SPECIAL_FILES = ["README.md", "__init__.py", "__main__.py"]
|
@@ -14,6 +17,7 @@ def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], Lis
|
|
14
17
|
content_parts = []
|
15
18
|
file_items = []
|
16
19
|
skipped_files = []
|
20
|
+
processed_files: Set[Path] = set() # Track processed files
|
17
21
|
console = Console()
|
18
22
|
|
19
23
|
# Load gitignore if it exists
|
@@ -34,7 +38,8 @@ def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], Lis
|
|
34
38
|
"""
|
35
39
|
if level > 1:
|
36
40
|
return
|
37
|
-
|
41
|
+
|
42
|
+
path = path.resolve()
|
38
43
|
relative_base = workdir
|
39
44
|
if path.is_dir():
|
40
45
|
relative_path = path.relative_to(relative_base)
|
@@ -43,8 +48,10 @@ def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], Lis
|
|
43
48
|
# Check for special files
|
44
49
|
special_found = []
|
45
50
|
for special_file in SPECIAL_FILES:
|
46
|
-
|
51
|
+
special_path = path / special_file
|
52
|
+
if special_path.exists() and special_path.resolve() not in processed_files:
|
47
53
|
special_found.append(special_file)
|
54
|
+
processed_files.add(special_path.resolve())
|
48
55
|
if special_found:
|
49
56
|
file_items[-1] = f"[blue]•[/blue] {relative_path}/ [cyan]({', '.join(special_found)})[/cyan]"
|
50
57
|
for special_file in special_found:
|
@@ -63,9 +70,15 @@ def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], Lis
|
|
63
70
|
rel_path = str(item.relative_to(workdir))
|
64
71
|
if gitignore_spec.match_file(rel_path):
|
65
72
|
continue
|
66
|
-
|
73
|
+
if item.resolve() not in processed_files: # Skip if already processed
|
74
|
+
scan_path(item, level+1)
|
67
75
|
|
68
76
|
else:
|
77
|
+
resolved_path = path.resolve()
|
78
|
+
if resolved_path in processed_files: # Skip if already processed
|
79
|
+
return
|
80
|
+
|
81
|
+
processed_files.add(resolved_path)
|
69
82
|
relative_path = path.relative_to(relative_base)
|
70
83
|
# check if file is binary
|
71
84
|
try:
|
@@ -97,6 +110,16 @@ def collect_files_content(paths: List[Path], workdir: Path = None) -> str:
|
|
97
110
|
if file_items and config.verbose:
|
98
111
|
console.print("\n[bold blue]Contents being analyzed:[/bold blue]")
|
99
112
|
console.print(Columns(file_items, padding=(0, 4), expand=True))
|
113
|
+
|
114
|
+
if config.verbose:
|
115
|
+
for part in content_parts:
|
116
|
+
if part.startswith('<file>'):
|
117
|
+
# Extract filename from XML content
|
118
|
+
path_start = part.find('<path>') + 6
|
119
|
+
path_end = part.find('</path>')
|
120
|
+
if path_start > 5 and path_end > path_start:
|
121
|
+
filepath = part[path_start:path_end]
|
122
|
+
console.print(f"[dim]Adding content from:[/dim] {filepath}")
|
100
123
|
|
101
124
|
return "\n".join(content_parts)
|
102
125
|
|
@@ -134,4 +157,20 @@ def preview_scan(paths: List[Path], workdir: Path = None) -> None:
|
|
134
157
|
|
135
158
|
def is_dir_empty(path: Path) -> bool:
|
136
159
|
"""Check if directory is empty, ignoring hidden files"""
|
137
|
-
return not any(item for item in path.iterdir() if not item.name.startswith('.'))
|
160
|
+
return not any(item for item in path.iterdir() if not item.name.startswith('.'))
|
161
|
+
|
162
|
+
def show_content_stats(content: str) -> None:
|
163
|
+
if not content:
|
164
|
+
return
|
165
|
+
|
166
|
+
dir_counts = defaultdict(int)
|
167
|
+
for line in content.split('\n'):
|
168
|
+
if line.startswith('<path>'):
|
169
|
+
path = Path(line.replace('<path>', '').replace('</path>', '').strip())
|
170
|
+
dir_counts[str(path.parent)] += 1
|
171
|
+
|
172
|
+
console = Console()
|
173
|
+
stats = [f"{directory} ({count} files)" for directory, count in dir_counts.items()]
|
174
|
+
columns = Columns(stats, equal=True, expand=True)
|
175
|
+
panel = Panel(columns, title="Work Context")
|
176
|
+
console.print(panel)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import pytest
|
2
|
+
from pathlib import Path
|
3
|
+
from janito.fileparser import validate_file_path, validate_file_content
|
4
|
+
|
5
|
+
def test_validate_file_path():
|
6
|
+
# Valid paths
|
7
|
+
assert validate_file_path(Path("test.py")) == (True, "")
|
8
|
+
assert validate_file_path(Path("folder/test.py")) == (True, "")
|
9
|
+
|
10
|
+
# Invalid paths
|
11
|
+
assert validate_file_path(Path("/absolute/path.py"))[0] == False
|
12
|
+
assert validate_file_path(Path("../escape.py"))[0] == False
|
13
|
+
assert validate_file_path(Path("test?.py"))[0] == False
|
14
|
+
assert validate_file_path(Path("test*.py"))[0] == False
|
15
|
+
|
16
|
+
def test_validate_file_content():
|
17
|
+
# Valid content
|
18
|
+
assert validate_file_content("print('hello')") == (True, "")
|
19
|
+
assert validate_file_content("# Empty file with comment\n") == (True, "")
|
20
|
+
|
21
|
+
# Invalid content
|
22
|
+
assert validate_file_content("")[0] == False
|
23
|
+
|
24
|
+
# Test large content
|
25
|
+
large_content = "x" * (1024 * 1024 + 1) # Slightly over 1MB
|
26
|
+
assert validate_file_content(large_content)[0] == False
|
@@ -0,0 +1,146 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: janito
|
3
|
+
Version: 0.5.0
|
4
|
+
Summary: A CLI tool for software development tasks powered by AI
|
5
|
+
Project-URL: Homepage, https://github.com/joaompinto/janito
|
6
|
+
Project-URL: Repository, https://github.com/joaompinto/janito.git
|
7
|
+
Author-email: João Pinto <lamego.pinto@gmail.com>
|
8
|
+
License: MIT
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Environment :: Console
|
11
|
+
Classifier: Intended Audience :: Developers
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
16
|
+
Classifier: Topic :: Software Development
|
17
|
+
Requires-Python: >=3.8
|
18
|
+
Requires-Dist: anthropic
|
19
|
+
Requires-Dist: pathspec
|
20
|
+
Requires-Dist: rich
|
21
|
+
Requires-Dist: tomli
|
22
|
+
Requires-Dist: typer
|
23
|
+
Description-Content-Type: text/markdown
|
24
|
+
|
25
|
+
# 🤖 Janito CLI
|
26
|
+
|
27
|
+
A CLI tool for software development tasks powered by AI. Janito is your friendly AI-powered software development buddy that helps with coding tasks like refactoring, documentation updates, and code optimization.
|
28
|
+
|
29
|
+
## 📥 Installation
|
30
|
+
|
31
|
+
1. Install using pip:
|
32
|
+
```bash
|
33
|
+
pip install janito
|
34
|
+
```
|
35
|
+
|
36
|
+
2. Verify installation:
|
37
|
+
```bash
|
38
|
+
janito --version
|
39
|
+
```
|
40
|
+
|
41
|
+
## ⚙️ Setup
|
42
|
+
|
43
|
+
1. Get your Anthropic API key from [Anthropic's website](https://www.anthropic.com/)
|
44
|
+
|
45
|
+
2. Set your API key:
|
46
|
+
```bash
|
47
|
+
# Linux/macOS
|
48
|
+
export ANTHROPIC_API_KEY='your-api-key-here'
|
49
|
+
|
50
|
+
# Windows (Command Prompt)
|
51
|
+
set ANTHROPIC_API_KEY=your-api-key-here
|
52
|
+
|
53
|
+
# Windows (PowerShell)
|
54
|
+
$env:ANTHROPIC_API_KEY='your-api-key-here'
|
55
|
+
```
|
56
|
+
|
57
|
+
3. (Optional) Configure default test command:
|
58
|
+
```bash
|
59
|
+
export JANITO_TEST_CMD='pytest' # or your preferred test command
|
60
|
+
```
|
61
|
+
|
62
|
+
## 🚀 Quick Start
|
63
|
+
|
64
|
+
### Basic Usage
|
65
|
+
|
66
|
+
```bash
|
67
|
+
# Add docstrings to your code
|
68
|
+
janito "add docstrings to this file"
|
69
|
+
|
70
|
+
# Optimize a function
|
71
|
+
janito "optimize the main function"
|
72
|
+
|
73
|
+
# Get code explanations
|
74
|
+
janito --ask "explain this code"
|
75
|
+
```
|
76
|
+
|
77
|
+
### Common Scenarios
|
78
|
+
|
79
|
+
1. **Code Refactoring**
|
80
|
+
```bash
|
81
|
+
# Refactor with test validation
|
82
|
+
janito "refactor this code to use list comprehension" --test "pytest"
|
83
|
+
|
84
|
+
# Refactor specific directory
|
85
|
+
janito "update imports" -i ./src
|
86
|
+
```
|
87
|
+
|
88
|
+
2. **Documentation Updates**
|
89
|
+
```bash
|
90
|
+
# Add or update docstrings
|
91
|
+
janito "add type hints and docstrings"
|
92
|
+
|
93
|
+
# Generate README
|
94
|
+
janito "create a README for this project"
|
95
|
+
```
|
96
|
+
|
97
|
+
3. **Code Analysis**
|
98
|
+
```bash
|
99
|
+
# Get code explanations
|
100
|
+
janito --ask "what does this function do?"
|
101
|
+
|
102
|
+
# Find potential improvements
|
103
|
+
janito --ask "suggest optimizations for this code"
|
104
|
+
```
|
105
|
+
|
106
|
+
## 🛠️ Command Reference
|
107
|
+
|
108
|
+
### Syntax
|
109
|
+
```bash
|
110
|
+
janito [OPTIONS] [REQUEST]
|
111
|
+
```
|
112
|
+
|
113
|
+
### Key Options
|
114
|
+
|
115
|
+
| Option | Description |
|
116
|
+
|--------|-------------|
|
117
|
+
| `REQUEST` | The AI request/instruction (in quotes) |
|
118
|
+
| `-w, --working-dir PATH` | Working directory [default: current] |
|
119
|
+
| `-i, --include PATH` | Include directory int the working context (can be multiple)|
|
120
|
+
| `--ask QUESTION` | Ask questions without making changes |
|
121
|
+
| `--test COMMAND` | Run tests before applying changes |
|
122
|
+
| `--debug` | Enable debug logging |
|
123
|
+
| `--verbose` | Enable verbose mode |
|
124
|
+
| `--version` | Show version information |
|
125
|
+
| `--help` | Show help message |
|
126
|
+
|
127
|
+
## 🔑 Key Features
|
128
|
+
|
129
|
+
- 🤖 AI-powered code analysis and modifications
|
130
|
+
- 💻 Interactive console mode
|
131
|
+
- ✅ Syntax validation for Python files
|
132
|
+
- 👀 Change preview and confirmation
|
133
|
+
- 🧪 Test command execution
|
134
|
+
- 📜 Change history tracking
|
135
|
+
|
136
|
+
## 📚 Additional Information
|
137
|
+
|
138
|
+
- Requires Python 3.8+
|
139
|
+
- Changes are backed up in `.janito/changes_history/`
|
140
|
+
- Environment variables:
|
141
|
+
- `ANTHROPIC_API_KEY`: Required for API access
|
142
|
+
- `JANITO_TEST_CMD`: Default test command (optional)
|
143
|
+
|
144
|
+
## 📄 License
|
145
|
+
|
146
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
@@ -0,0 +1,45 @@
|
|
1
|
+
janito/__init__.py,sha256=T1hTuIOJgEUjXNHplUuMZIgG_QxC1RH2ZcBe6WJ1XBE,1099
|
2
|
+
janito/__main__.py,sha256=ogcCBQ2YHkQhdhrXF7OV-DthLiL-Zw24XtRRYscT2Yo,2018
|
3
|
+
janito/_contextparser.py,sha256=iDX6nlqUQr-lj7lkEI4B6jHLci_Kxl-XWOaEiAQtVxA,4531
|
4
|
+
janito/changehistory.py,sha256=4Mu60og1pySlyfB36aqHnNKyH0yuSj-5rvBjmmvKGYw,1510
|
5
|
+
janito/common.py,sha256=imIwx0FCDe7p71ze9TBvn_W0Qz99Am3X5U9eHYd9vws,790
|
6
|
+
janito/config.py,sha256=ocg0lyab9ysgczKaqJTAtv0ZRa2VDMwclTJBgps7Vxw,1171
|
7
|
+
janito/fileparser.py,sha256=rJEme9QWbtnsjs9KuO3YI7B499DnfedWtzBONXqrjfU,12925
|
8
|
+
janito/prompts.py,sha256=M8IveFkG3fgLe87_9kTNdWAVkqmsD0T4eP6VmOqHAOQ,2607
|
9
|
+
janito/qa.py,sha256=MSzu9yvLCwXr9sKylTXtMfTy_2fRfPl3CU2NXf0OrS4,1977
|
10
|
+
janito/review.py,sha256=5Oc6BfxMGNmKbIeDP5_EiAKUDeQwVOD0YL7iqfgJLRE,471
|
11
|
+
janito/scan.py,sha256=QBXu64t8CjjJhiFdO-0npwdSPys9IX1oOOOyh1cGVIE,7678
|
12
|
+
janito/version.py,sha256=ylfPwGtdY8dEOFJ-DB9gKUQLggqRCvoLxhpnwjzCM94,739
|
13
|
+
janito/agents/__init__.py,sha256=VPBXIh526D11NrCSnuXUerYT7AI3rynp2klCgz94tPk,648
|
14
|
+
janito/agents/agent.py,sha256=3uGiUrvj9oCW6_oK-oMQQJ77K8jZFv7mAdXlIG1dxNY,751
|
15
|
+
janito/agents/claudeai.py,sha256=bl1MeALicf6arX_m9GmtAj0i3UoUiTbbXytVjLam2S8,2395
|
16
|
+
janito/agents/openai.py,sha256=tNtlzFJMWFv38Z62opR23u6xXlZ9L4xX_mf2f3wjrLU,2082
|
17
|
+
janito/agents/test.py,sha256=xoN1q9DUSYpUbnvTP1qZsEfxYrZfocJlt9DkIuMDvvY,1552
|
18
|
+
janito/analysis/__init__.py,sha256=QVhIoZdOc4eYhQ9ZRoZZiwowUaa-PQ0_7HV_cx4eOZU,734
|
19
|
+
janito/analysis/display.py,sha256=714423TxelieAyBe0M5A78i99qj_VfggZUWRbk2owzU,5204
|
20
|
+
janito/analysis/options.py,sha256=tgsLZOtNy35l4v4F6p_adGk3CrWhrDN0Ba_tw4Zzj5Q,3852
|
21
|
+
janito/analysis/prompts.py,sha256=7NI7XyL6gUeJWTfVP_X0ohtSnYb6gpu4Thy5kxzoeD8,2454
|
22
|
+
janito/change/__init__.py,sha256=ElyiGt6KmwufQouir-0l0ZtxNZdeLWTFRc4Vbg0RW3s,467
|
23
|
+
janito/change/applier.py,sha256=GXs0DRfb48WNoSyAH0uETjpzzFLvmUZGkb6VydKzazk,11601
|
24
|
+
janito/change/content.py,sha256=R8lbOFF2ANV_NF3S6zCV60-Q7hGFWiS-zmJBgu-r5SU,2475
|
25
|
+
janito/change/indentation.py,sha256=qvsGX9ZPm5cRftXJSKSVbK9-Po_95HCAc7sG98BxhIY,1191
|
26
|
+
janito/change/position.py,sha256=Nlkg0sGUvqYwtKA19ne1kT3ixiVYKUcy_40Cdr-gfl8,6385
|
27
|
+
janito/changeviewer/__init__.py,sha256=5kR3fsAYSgnA0Hlty0fMC6392aSK7WifH2ZIW-INjBc,298
|
28
|
+
janito/changeviewer/diff.py,sha256=2nW1Yzu7rzzIsuHWg-t2EJfsoVdbRaRJk3ewQBjQdxY,1117
|
29
|
+
janito/changeviewer/panels.py,sha256=F1yWUd6Vh535rV20NnFrVh6uvpc3w5CDBSNQUf_vOC8,9426
|
30
|
+
janito/changeviewer/styling.py,sha256=qv7DVfu57Qy93XUWIWdyqZWhgcfm-BaJVPslSuch05s,2461
|
31
|
+
janito/changeviewer/themes.py,sha256=BWa4l82fNM0pj01UYK9q_C7t_AqTwNbR8i0-F4I7KI8,1593
|
32
|
+
janito/cli/__init__.py,sha256=3gyMSaEAH2N4mXfZTLsHXKxXDxdhlYUeAPYQuhnOVBE,77
|
33
|
+
janito/cli/commands.py,sha256=6vPT60oT-1jJd_qdy-Si_pcf7yMCGmwpvQ43FZNJjUs,2001
|
34
|
+
janito/cli/functions.py,sha256=5Nn4muPv1KrInhcernByQqn_T0q2bCxPeDA1BoPw4K8,11399
|
35
|
+
janito/cli/registry.py,sha256=R1sI45YonxjMSLhAle7Dt18X_devrMsLt0ljb-rNza4,690
|
36
|
+
janito/console/__init__.py,sha256=0zxlJR88ESfuXtyzARnWNKcZ1rTyWLZwzXrfDQ7NHyw,77
|
37
|
+
janito/console/commands.py,sha256=kuI2w1LfEw0kbF_vOAYVENoMQYC42fXmDFZgxVKJKo8,3422
|
38
|
+
janito/console/core.py,sha256=DTAP_bhw_DTklUyPbGdjSzllUBMg7jXW0SklXkRNMI8,2264
|
39
|
+
janito/console/display.py,sha256=VNyHGHA_MG-dLPTCylgOKgHAl0muwhmsvg3nXFU-OZo,5380
|
40
|
+
janito/tests/test_fileparser.py,sha256=20CfwfnFtVm3_qU11qRAdcBZQXaLlkcHgbqWDOeazj4,997
|
41
|
+
janito-0.5.0.dist-info/METADATA,sha256=Jq4fJLGNs-4O8IOB2iy_8JrszlWbFzOmwrZRjQxIs_Y,3659
|
42
|
+
janito-0.5.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
43
|
+
janito-0.5.0.dist-info/entry_points.txt,sha256=wIo5zZxbmu4fC-ZMrsKD0T0vq7IqkOOLYhrqRGypkx4,48
|
44
|
+
janito-0.5.0.dist-info/licenses/LICENSE,sha256=xLIUXRPjtsgQml2zD1Pn4LpgiyZ49raw6jZDlO_gZdo,1062
|
45
|
+
janito-0.5.0.dist-info/RECORD,,
|
janito/analysis.py
DELETED
@@ -1,281 +0,0 @@
|
|
1
|
-
"""Analysis display module for Janito.
|
2
|
-
|
3
|
-
This module handles the formatting and display of analysis results, option selection,
|
4
|
-
and related functionality for the Janito application.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from typing import Optional, Dict, List, Tuple
|
8
|
-
from pathlib import Path
|
9
|
-
from rich.console import Console
|
10
|
-
from rich.markdown import Markdown
|
11
|
-
from rich.panel import Panel
|
12
|
-
from rich.text import Text
|
13
|
-
from rich import box
|
14
|
-
from rich.columns import Columns
|
15
|
-
from rich.rule import Rule
|
16
|
-
from rich.prompt import Prompt
|
17
|
-
from janito.claude import ClaudeAPIAgent
|
18
|
-
from janito.scan import collect_files_content
|
19
|
-
from janito.common import progress_send_message
|
20
|
-
from janito.config import config
|
21
|
-
from dataclasses import dataclass
|
22
|
-
import re
|
23
|
-
|
24
|
-
MIN_PANEL_WIDTH = 40 # Minimum width for each panel
|
25
|
-
|
26
|
-
def get_history_file_type(filepath: Path) -> str:
|
27
|
-
"""Determine the type of saved file based on its name"""
|
28
|
-
name = filepath.name.lower()
|
29
|
-
if 'changes' in name:
|
30
|
-
return 'changes'
|
31
|
-
elif 'selected' in name:
|
32
|
-
return 'selected'
|
33
|
-
elif 'analysis' in name:
|
34
|
-
return 'analysis'
|
35
|
-
elif 'response' in name:
|
36
|
-
return 'response'
|
37
|
-
return 'unknown'
|
38
|
-
|
39
|
-
@dataclass
|
40
|
-
class AnalysisOption:
|
41
|
-
letter: str
|
42
|
-
summary: str
|
43
|
-
affected_files: List[str]
|
44
|
-
description_items: List[str] # Changed from description to description_items
|
45
|
-
|
46
|
-
CHANGE_ANALYSIS_PROMPT = """
|
47
|
-
Current files:
|
48
|
-
<files>
|
49
|
-
{files_content}
|
50
|
-
</files>
|
51
|
-
|
52
|
-
Considering the above current files content, provide options for the requested change in the following format:
|
53
|
-
|
54
|
-
A. Keyword summary of the change
|
55
|
-
-----------------
|
56
|
-
Description:
|
57
|
-
- Detailed description of the change
|
58
|
-
|
59
|
-
Affected files:
|
60
|
-
- file1.py
|
61
|
-
- file2.py (new)
|
62
|
-
-----------------
|
63
|
-
END_OF_OPTIONS (mandatory marker)
|
64
|
-
|
65
|
-
RULES:
|
66
|
-
- do NOT provide the content of the files
|
67
|
-
- do NOT offer to implement the changes
|
68
|
-
|
69
|
-
Request:
|
70
|
-
{request}
|
71
|
-
"""
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
def prompt_user(message: str, choices: List[str] = None) -> str:
|
77
|
-
"""Display a prominent user prompt with optional choices"""
|
78
|
-
console = Console()
|
79
|
-
console.print()
|
80
|
-
console.print(Rule(" User Input Required ", style="bold cyan"))
|
81
|
-
|
82
|
-
if choices:
|
83
|
-
choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
|
84
|
-
console.print(Panel(choice_text, box=box.ROUNDED))
|
85
|
-
|
86
|
-
return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
|
87
|
-
|
88
|
-
def validate_option_letter(letter: str, options: dict) -> bool:
|
89
|
-
"""Validate if the given letter is a valid option or 'M' for modify"""
|
90
|
-
return letter.upper() in options or letter.upper() == 'M'
|
91
|
-
|
92
|
-
def get_option_selection() -> str:
|
93
|
-
"""Get user input for option selection with modify option"""
|
94
|
-
console = Console()
|
95
|
-
console.print("\n[cyan]Enter option letter or 'M' to modify request[/cyan]")
|
96
|
-
while True:
|
97
|
-
letter = prompt_user("Select option").strip().upper()
|
98
|
-
if letter == 'M' or (letter.isalpha() and len(letter) == 1):
|
99
|
-
return letter
|
100
|
-
console.print("[red]Please enter a valid letter or 'M'[/red]")
|
101
|
-
|
102
|
-
def _display_options(options: Dict[str, AnalysisOption]) -> None:
|
103
|
-
"""Display available options with left-aligned content and horizontally centered panels."""
|
104
|
-
console = Console()
|
105
|
-
|
106
|
-
# Display centered title using Rule
|
107
|
-
console.print()
|
108
|
-
console.print(Rule(" Available Options ", style="bold cyan", align="center"))
|
109
|
-
console.print()
|
110
|
-
|
111
|
-
# Calculate optimal width based on terminal
|
112
|
-
term_width = console.width or 100
|
113
|
-
panel_width = max(MIN_PANEL_WIDTH, (term_width // 2) - 10) # Width for two columns
|
114
|
-
|
115
|
-
# Create panels for each option
|
116
|
-
panels = []
|
117
|
-
for letter, option in options.items():
|
118
|
-
content = Text()
|
119
|
-
|
120
|
-
# Display description as bullet points
|
121
|
-
content.append("Description:\n", style="bold cyan")
|
122
|
-
for item in option.description_items:
|
123
|
-
content.append(f"• {item}\n", style="white")
|
124
|
-
content.append("\n")
|
125
|
-
|
126
|
-
# Display affected files
|
127
|
-
if option.affected_files:
|
128
|
-
content.append("Affected files:\n", style="bold cyan")
|
129
|
-
for file in option.affected_files:
|
130
|
-
content.append(f"• {file}\n", style="yellow")
|
131
|
-
|
132
|
-
# Create panel with consistent styling
|
133
|
-
panel = Panel(
|
134
|
-
content,
|
135
|
-
box=box.ROUNDED,
|
136
|
-
border_style="cyan",
|
137
|
-
title=f"Option {letter}: {option.summary}",
|
138
|
-
title_align="center",
|
139
|
-
padding=(1, 2),
|
140
|
-
width=panel_width
|
141
|
-
)
|
142
|
-
panels.append(panel)
|
143
|
-
|
144
|
-
# Display panels in columns with center alignment
|
145
|
-
if panels:
|
146
|
-
# Group panels into pairs for two columns
|
147
|
-
for i in range(0, len(panels), 2):
|
148
|
-
pair = panels[i:i+2]
|
149
|
-
columns = Columns(
|
150
|
-
pair,
|
151
|
-
align="center",
|
152
|
-
expand=True,
|
153
|
-
equal=True,
|
154
|
-
padding=(0, 2)
|
155
|
-
)
|
156
|
-
console.print(columns)
|
157
|
-
console.print() # Add spacing between rows
|
158
|
-
|
159
|
-
def _display_markdown(content: str) -> None:
|
160
|
-
"""Display content in markdown format."""
|
161
|
-
console = Console()
|
162
|
-
md = Markdown(content)
|
163
|
-
console.print(md)
|
164
|
-
|
165
|
-
def _display_raw_history(claude: ClaudeAPIAgent) -> None:
|
166
|
-
"""Display raw message history from Claude agent."""
|
167
|
-
console = Console()
|
168
|
-
console.print("\n=== Message History ===")
|
169
|
-
for role, content in claude.messages_history:
|
170
|
-
console.print(f"\n[bold cyan]{role.upper()}:[/bold cyan]")
|
171
|
-
console.print(content)
|
172
|
-
console.print("\n=== End Message History ===\n")
|
173
|
-
|
174
|
-
|
175
|
-
def format_analysis(analysis: str, raw: bool = False, claude: Optional[ClaudeAPIAgent] = None, workdir: Optional[Path] = None) -> None:
|
176
|
-
"""Format and display the analysis output with enhanced capabilities."""
|
177
|
-
console = Console()
|
178
|
-
|
179
|
-
if raw and claude:
|
180
|
-
_display_raw_history(claude)
|
181
|
-
else:
|
182
|
-
options = parse_analysis_options(analysis)
|
183
|
-
if options:
|
184
|
-
_display_options(options)
|
185
|
-
else:
|
186
|
-
console.print("\n[yellow]Warning: No valid options found in response. Displaying as markdown.[/yellow]\n")
|
187
|
-
_display_markdown(analysis)
|
188
|
-
|
189
|
-
def get_history_path(workdir: Path) -> Path:
|
190
|
-
"""Create and return the history directory path"""
|
191
|
-
history_dir = workdir / '.janito' / 'history'
|
192
|
-
history_dir.mkdir(parents=True, exist_ok=True)
|
193
|
-
return history_dir
|
194
|
-
|
195
|
-
def get_timestamp() -> str:
|
196
|
-
"""Get current UTC timestamp in YMD_HMS format with leading zeros"""
|
197
|
-
from datetime import datetime, timezone
|
198
|
-
return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
199
|
-
|
200
|
-
def save_to_file(content: str, prefix: str, workdir: Path) -> Path:
|
201
|
-
"""Save content to a timestamped file in history directory"""
|
202
|
-
history_dir = get_history_path(workdir)
|
203
|
-
timestamp = get_timestamp()
|
204
|
-
filename = f"{timestamp}_{prefix}.txt"
|
205
|
-
file_path = history_dir / filename
|
206
|
-
file_path.write_text(content)
|
207
|
-
return file_path
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
def parse_analysis_options(response: str) -> dict[str, AnalysisOption]:
|
212
|
-
"""Parse options from the response text using a line-based approach."""
|
213
|
-
options = {}
|
214
|
-
|
215
|
-
# Extract content up to END_OF_OPTIONS
|
216
|
-
if 'END_OF_OPTIONS' in response:
|
217
|
-
response = response.split('END_OF_OPTIONS')[0]
|
218
|
-
|
219
|
-
lines = response.splitlines()
|
220
|
-
current_option = None
|
221
|
-
current_section = None
|
222
|
-
|
223
|
-
for line in lines:
|
224
|
-
line = line.strip()
|
225
|
-
if not line:
|
226
|
-
continue
|
227
|
-
|
228
|
-
# Check for new option starting with letter
|
229
|
-
if len(line) >= 2 and line[0].isalpha() and line[1] == '.' and line[0].isupper():
|
230
|
-
if current_option:
|
231
|
-
options[current_option.letter] = current_option
|
232
|
-
|
233
|
-
letter = line[0]
|
234
|
-
summary = line[2:].strip()
|
235
|
-
current_option = AnalysisOption(
|
236
|
-
letter=letter,
|
237
|
-
summary=summary,
|
238
|
-
affected_files=[],
|
239
|
-
description_items=[]
|
240
|
-
)
|
241
|
-
current_section = None
|
242
|
-
continue
|
243
|
-
|
244
|
-
# Skip separator lines
|
245
|
-
if line.startswith('---'):
|
246
|
-
continue
|
247
|
-
|
248
|
-
# Check for section headers
|
249
|
-
if line.startswith('Description:'):
|
250
|
-
current_section = 'description'
|
251
|
-
continue
|
252
|
-
elif line.startswith('Affected files:'):
|
253
|
-
current_section = 'files'
|
254
|
-
continue
|
255
|
-
|
256
|
-
# Process content based on current section
|
257
|
-
if current_option and current_section and line:
|
258
|
-
if current_section == 'description':
|
259
|
-
# Strip bullet points and whitespace
|
260
|
-
item = line.lstrip(' -•').strip()
|
261
|
-
if item:
|
262
|
-
current_option.description_items.append(item)
|
263
|
-
elif current_section == 'files':
|
264
|
-
# Strip bullet points and (modified)/(new) annotations
|
265
|
-
file_path = line.lstrip(' -')
|
266
|
-
file_path = re.sub(r'\s*\([^)]+\)\s*$', '', file_path)
|
267
|
-
if file_path:
|
268
|
-
current_option.affected_files.append(file_path)
|
269
|
-
|
270
|
-
# Add the last option if exists
|
271
|
-
if current_option:
|
272
|
-
options[current_option.letter] = current_option
|
273
|
-
|
274
|
-
return options
|
275
|
-
|
276
|
-
def build_request_analysis_prompt(files_content: str, request: str) -> str:
|
277
|
-
"""Build prompt for information requests"""
|
278
|
-
return CHANGE_ANALYSIS_PROMPT.format(
|
279
|
-
files_content=files_content,
|
280
|
-
request=request
|
281
|
-
)
|