janito 0.1.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 +6 -0
- janito/__main__.py +9 -0
- janito/change.py +382 -0
- janito/claude.py +112 -0
- janito/commands.py +377 -0
- janito/console.py +354 -0
- janito/janito.py +354 -0
- janito/prompts.py +181 -0
- janito/watcher.py +82 -0
- janito/workspace.py +169 -0
- janito/xmlchangeparser.py +202 -0
- janito-0.1.0.dist-info/LICENSE +21 -0
- janito-0.1.0.dist-info/METADATA +106 -0
- janito-0.1.0.dist-info/RECORD +19 -0
- janito-0.1.0.dist-info/WHEEL +5 -0
- janito-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +4 -0
- tests/conftest.py +9 -0
- tests/test_change.py +393 -0
janito/workspace.py
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import List, Dict
|
3
|
+
|
4
|
+
class Workspace:
|
5
|
+
def __init__(self, base_path: Path = None):
|
6
|
+
self.base_path = base_path or Path().absolute()
|
7
|
+
self.default_exclude = [".janito", "__pycache__", ".git", ".janito/history"] # Added .janito/history
|
8
|
+
self.default_patterns = ["*.py", "*.txt", "*.md", "*.toml", "**/.gitignore"]
|
9
|
+
self.history_file = self.base_path / ".janito" / "history"
|
10
|
+
# Create .janito directory if it doesn't exist
|
11
|
+
(self.base_path / ".janito").mkdir(exist_ok=True)
|
12
|
+
|
13
|
+
def generate_file_structure(self, pattern: str = None, exclude_patterns: List[str] = None) -> Dict:
|
14
|
+
"""Generate a tree structure of files in the workspace directory."""
|
15
|
+
exclude_patterns = exclude_patterns or self.default_exclude
|
16
|
+
patterns = [pattern] if pattern else self.default_patterns
|
17
|
+
|
18
|
+
tree = {}
|
19
|
+
try:
|
20
|
+
base_path = self.base_path.resolve()
|
21
|
+
|
22
|
+
for pattern in patterns:
|
23
|
+
for file in sorted(base_path.rglob(pattern)):
|
24
|
+
try:
|
25
|
+
import fnmatch
|
26
|
+
|
27
|
+
if any(fnmatch.fnmatch(str(file), pat) for pat in exclude_patterns):
|
28
|
+
continue
|
29
|
+
|
30
|
+
try:
|
31
|
+
file.relative_to(base_path)
|
32
|
+
except ValueError:
|
33
|
+
continue
|
34
|
+
|
35
|
+
rel_path = file.relative_to(base_path)
|
36
|
+
current = tree
|
37
|
+
|
38
|
+
for part in rel_path.parts[:-1]:
|
39
|
+
if part not in current:
|
40
|
+
current[part] = {}
|
41
|
+
current = current[part]
|
42
|
+
current[rel_path.parts[-1]] = None
|
43
|
+
|
44
|
+
except Exception as e:
|
45
|
+
print(f"Error processing file {file}: {e}")
|
46
|
+
continue
|
47
|
+
|
48
|
+
except Exception as e:
|
49
|
+
print(f"Error generating file structure: {e}")
|
50
|
+
return {}
|
51
|
+
|
52
|
+
return tree
|
53
|
+
|
54
|
+
def get_files_content(self, exclude_patterns: List[str] = None) -> str:
|
55
|
+
"""Get content of files in workspace directory in XML format"""
|
56
|
+
content = ['<workspaceFiles>']
|
57
|
+
exclude_patterns = exclude_patterns or self.default_exclude
|
58
|
+
base_path = self.base_path.resolve()
|
59
|
+
|
60
|
+
for pattern in self.default_patterns:
|
61
|
+
for file in sorted(base_path.rglob(pattern)):
|
62
|
+
if any(pat in str(file) for pat in exclude_patterns):
|
63
|
+
continue
|
64
|
+
|
65
|
+
try:
|
66
|
+
rel_path = file.relative_to(base_path)
|
67
|
+
content.append(f' <file path="{rel_path}">')
|
68
|
+
content.append(' <content>')
|
69
|
+
content.append(file.read_text()) # Remove .strip() to preserve original content
|
70
|
+
content.append(' </content>')
|
71
|
+
content.append(' </file>')
|
72
|
+
except ValueError:
|
73
|
+
continue
|
74
|
+
|
75
|
+
content.append('</workspaceFiles>')
|
76
|
+
return "\n".join(content)
|
77
|
+
|
78
|
+
def format_tree(self, tree: Dict, prefix: str = "", is_last: bool = True) -> List[str]:
|
79
|
+
"""Format a tree dictionary into a list of strings showing the structure."""
|
80
|
+
lines = []
|
81
|
+
|
82
|
+
if not tree:
|
83
|
+
return lines
|
84
|
+
|
85
|
+
for i, (name, subtree) in enumerate(tree.items()):
|
86
|
+
is_last_item = i == len(tree) - 1
|
87
|
+
connector = "└── " if is_last_item else "├── "
|
88
|
+
|
89
|
+
if subtree is None: # File
|
90
|
+
lines.append(f"{prefix}{connector}{name}")
|
91
|
+
else: # Directory
|
92
|
+
lines.append(f"{prefix}{connector}{name}/")
|
93
|
+
next_prefix = prefix + (" " if is_last_item else "│ ")
|
94
|
+
lines.extend(self.format_tree(subtree, next_prefix))
|
95
|
+
|
96
|
+
return lines
|
97
|
+
|
98
|
+
def get_workspace_status(self) -> str:
|
99
|
+
"""Get a formatted string of the workspace structure"""
|
100
|
+
tree = self.generate_file_structure()
|
101
|
+
if not tree:
|
102
|
+
return "No files found in the current workspace."
|
103
|
+
tree_lines = self.format_tree(tree)
|
104
|
+
return "Files in workspace:\n" + "\n".join(tree_lines)
|
105
|
+
|
106
|
+
def get_excluded_files(self) -> List[str]:
|
107
|
+
"""Get a list of files and directories that are excluded from the workspace"""
|
108
|
+
exclude_patterns = self.default_exclude
|
109
|
+
gitignore_paths = list(self.base_path.rglob(".gitignore"))
|
110
|
+
for p in gitignore_paths:
|
111
|
+
with open(p) as f:
|
112
|
+
ignore_patterns = [line.strip() for line in f if line.strip() and not line.strip().startswith("#")]
|
113
|
+
exclude_patterns.extend(ignore_patterns)
|
114
|
+
|
115
|
+
excluded_files = []
|
116
|
+
base_path = self.base_path.resolve()
|
117
|
+
|
118
|
+
for file in base_path.rglob('*'):
|
119
|
+
if any(fnmatch.fnmatch(str(file), pat) for pat in exclude_patterns):
|
120
|
+
try:
|
121
|
+
rel_path = file.relative_to(base_path)
|
122
|
+
excluded_files.append(str(rel_path))
|
123
|
+
except ValueError:
|
124
|
+
continue
|
125
|
+
|
126
|
+
return excluded_files
|
127
|
+
|
128
|
+
def print_excluded_files(self):
|
129
|
+
"""Print files and patterns excluded from workspace"""
|
130
|
+
excluded_files = self.get_excluded_files()
|
131
|
+
if not excluded_files:
|
132
|
+
self.console.print("No excluded files or directories found.")
|
133
|
+
return
|
134
|
+
|
135
|
+
self.console.print("\nExcluded files and directories:")
|
136
|
+
for path in excluded_files:
|
137
|
+
self.console.print(f" {path}")
|
138
|
+
|
139
|
+
def calculate_stats(self) -> Dict[str, int]:
|
140
|
+
"""Calculate statistics about the current directory contents."""
|
141
|
+
stats = {
|
142
|
+
"total_files": 0,
|
143
|
+
"total_dirs": 0,
|
144
|
+
"total_size": 0
|
145
|
+
}
|
146
|
+
for path in self.base_path.rglob('*'):
|
147
|
+
if path.is_file():
|
148
|
+
stats["total_files"] += 1
|
149
|
+
stats["total_size"] += path.stat().st_size
|
150
|
+
elif path.is_dir():
|
151
|
+
stats["total_dirs"] += 1
|
152
|
+
return stats
|
153
|
+
|
154
|
+
|
155
|
+
def print_workspace_structure(self):
|
156
|
+
"""Print the workspace structure with statistics"""
|
157
|
+
stats = self.calculate_stats()
|
158
|
+
tree = self.generate_file_structure()
|
159
|
+
if not tree:
|
160
|
+
print("No files found in the current workspace.")
|
161
|
+
return
|
162
|
+
tree_lines = self.format_tree(tree)
|
163
|
+
print("Workspace structure:")
|
164
|
+
print("=" * 80)
|
165
|
+
print(f"Total files: {stats['total_files']}")
|
166
|
+
print(f"Total directories: {stats['total_dirs']}")
|
167
|
+
print(f"Total size: {stats['total_size']} bytes")
|
168
|
+
print("=" * 80)
|
169
|
+
print("\n".join(tree_lines))
|
@@ -0,0 +1,202 @@
|
|
1
|
+
from typing import List
|
2
|
+
from pathlib import Path
|
3
|
+
import re
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from rich.console import Console
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class XMLBlock:
|
9
|
+
"""Simple container for parsed XML blocks"""
|
10
|
+
description: str = ""
|
11
|
+
old_content: List[str] = None
|
12
|
+
new_content: List[str] = None
|
13
|
+
|
14
|
+
def __post_init__(self):
|
15
|
+
if self.old_content is None:
|
16
|
+
self.old_content = []
|
17
|
+
if self.new_content is None:
|
18
|
+
self.new_content = []
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class XMLChange:
|
22
|
+
"""Simple container for parsed XML changes"""
|
23
|
+
path: Path
|
24
|
+
operation: str
|
25
|
+
blocks: List[XMLBlock] = None
|
26
|
+
content: str = ""
|
27
|
+
|
28
|
+
def __post_init__(self):
|
29
|
+
if self.blocks is None:
|
30
|
+
self.blocks = []
|
31
|
+
|
32
|
+
class XMLChangeParser:
|
33
|
+
"""XML parser for file changes"""
|
34
|
+
def __init__(self):
|
35
|
+
self.console = Console()
|
36
|
+
self.current_operation = None
|
37
|
+
self.has_invalid_tags = False # Track invalid tag occurrences
|
38
|
+
|
39
|
+
def _validate_tag_format(self, line: str) -> bool:
|
40
|
+
"""Validate that a line contains only a single XML tag and nothing else"""
|
41
|
+
stripped = line.strip()
|
42
|
+
if not stripped:
|
43
|
+
return True
|
44
|
+
if stripped.startswith('<?xml'):
|
45
|
+
return True
|
46
|
+
|
47
|
+
# Allow empty content tags in one line
|
48
|
+
if stripped in ('<oldContent></oldContent>', '<newContent></newContent>'):
|
49
|
+
return True
|
50
|
+
|
51
|
+
# Check if line contains exactly one XML tag and nothing else
|
52
|
+
return bool(re.match(r'^\s*<[^>]+>\s*$', line))
|
53
|
+
|
54
|
+
def _validate_path(self, path_str: str) -> bool:
|
55
|
+
"""Validate that path is relative and does not try to escape workspace"""
|
56
|
+
try:
|
57
|
+
path = Path(path_str)
|
58
|
+
# Check if path is absolute
|
59
|
+
if path.is_absolute():
|
60
|
+
self.console.print(f"[red]Error: Path must be relative: {path_str}[/]")
|
61
|
+
return False
|
62
|
+
# Check for path traversal attempts
|
63
|
+
if '..' in path.parts:
|
64
|
+
self.console.print(f"[red]Error: Path cannot contain '..': {path_str}[/]")
|
65
|
+
return False
|
66
|
+
return True
|
67
|
+
except Exception:
|
68
|
+
self.console.print(f"[red]Error: Invalid path format: {path_str}[/]")
|
69
|
+
return False
|
70
|
+
|
71
|
+
def parse_response(self, response: str) -> List[XMLChange]:
|
72
|
+
"""Parse XML response according to format specification:
|
73
|
+
<fileChanges>
|
74
|
+
<change path="file.py" operation="create|modify">
|
75
|
+
<block description="Description of changes">
|
76
|
+
<oldContent>
|
77
|
+
// Exact content to be replaced (empty for create/append)
|
78
|
+
// Must match existing indentation exactly
|
79
|
+
</oldContent>
|
80
|
+
<newContent>
|
81
|
+
// New content to replace the old content
|
82
|
+
// Must include desired indentation
|
83
|
+
</newContent>
|
84
|
+
</block>
|
85
|
+
</change>
|
86
|
+
</fileChanges>
|
87
|
+
"""
|
88
|
+
changes = []
|
89
|
+
current_change = None
|
90
|
+
current_block = None
|
91
|
+
current_section = None
|
92
|
+
content_buffer = []
|
93
|
+
in_content = False
|
94
|
+
self.current_operation = None
|
95
|
+
self.has_invalid_tags = False # Reset at start of parsing
|
96
|
+
|
97
|
+
try:
|
98
|
+
lines = response.splitlines()
|
99
|
+
for i, line in enumerate(lines):
|
100
|
+
stripped = line.strip()
|
101
|
+
|
102
|
+
# Update operation tracking when encountering a change tag
|
103
|
+
if match := re.match(r'<change\s+path="([^"]+)"\s+operation="([^"]+)">', stripped):
|
104
|
+
_, operation = match.groups()
|
105
|
+
self.current_operation = operation
|
106
|
+
self.has_invalid_tags = False # Reset for new change
|
107
|
+
|
108
|
+
# Reset operation on change end
|
109
|
+
elif stripped == '</change>':
|
110
|
+
if current_change and not self.has_invalid_tags:
|
111
|
+
changes.append(current_change)
|
112
|
+
current_change = None
|
113
|
+
self.current_operation = None
|
114
|
+
continue
|
115
|
+
|
116
|
+
# Validate tag format
|
117
|
+
if not self._validate_tag_format(line) and not in_content:
|
118
|
+
self.console.print(f"[red]Invalid tag format at line {i+1}: {line}[/]")
|
119
|
+
self.has_invalid_tags = True
|
120
|
+
continue
|
121
|
+
|
122
|
+
if not stripped and not in_content:
|
123
|
+
continue
|
124
|
+
|
125
|
+
if stripped.startswith('<fileChanges>'):
|
126
|
+
continue
|
127
|
+
elif stripped.startswith('</fileChanges>'):
|
128
|
+
break
|
129
|
+
|
130
|
+
elif match := re.match(r'<change\s+path="([^"]+)"\s+operation="([^"]+)">', stripped):
|
131
|
+
path, operation = match.groups()
|
132
|
+
# Validate path before creating change object
|
133
|
+
if not self._validate_path(path):
|
134
|
+
self.has_invalid_tags = True
|
135
|
+
continue
|
136
|
+
if operation not in ('create', 'modify'):
|
137
|
+
self.console.print(f"[red]Invalid operation '{operation}' - skipping change[/]")
|
138
|
+
continue
|
139
|
+
current_change = XMLChange(Path(path), operation)
|
140
|
+
current_block = None
|
141
|
+
elif stripped == '</change>':
|
142
|
+
if current_change:
|
143
|
+
changes.append(current_change)
|
144
|
+
current_change = None
|
145
|
+
continue
|
146
|
+
|
147
|
+
elif match := re.match(r'<block\s+description="([^"]+)">', stripped):
|
148
|
+
if current_change:
|
149
|
+
current_block = XMLBlock(description=match.group(1))
|
150
|
+
elif stripped == '</block>':
|
151
|
+
if current_change and current_block:
|
152
|
+
current_change.blocks.append(current_block)
|
153
|
+
current_block = None
|
154
|
+
continue
|
155
|
+
|
156
|
+
elif stripped in ('<oldContent>', '<newContent>'):
|
157
|
+
if current_block:
|
158
|
+
current_section = 'old' if 'old' in stripped else 'new'
|
159
|
+
content_buffer = []
|
160
|
+
in_content = True
|
161
|
+
elif stripped in ('</oldContent>', '</newContent>'):
|
162
|
+
if current_block and in_content:
|
163
|
+
# Find the common indentation of non-empty lines
|
164
|
+
non_empty_lines = [line for line in content_buffer if line.strip()]
|
165
|
+
if non_empty_lines:
|
166
|
+
# Find minimal indent by looking at first real line
|
167
|
+
first_line = next(line for line in content_buffer if line.strip())
|
168
|
+
indent = len(first_line) - len(first_line.lstrip())
|
169
|
+
# Remove only the common indentation from XML
|
170
|
+
content = []
|
171
|
+
for line in content_buffer:
|
172
|
+
if line.strip():
|
173
|
+
# Remove only the XML indentation
|
174
|
+
content.append(line[indent:])
|
175
|
+
elif content: # Keep empty lines only if we have previous content
|
176
|
+
content.append('')
|
177
|
+
else:
|
178
|
+
content = []
|
179
|
+
|
180
|
+
if current_section == 'old':
|
181
|
+
current_block.old_content = content
|
182
|
+
else:
|
183
|
+
current_block.new_content = content
|
184
|
+
in_content = False
|
185
|
+
current_section = None
|
186
|
+
continue
|
187
|
+
|
188
|
+
elif in_content:
|
189
|
+
# Store lines with their original indentation
|
190
|
+
content_buffer.append(line)
|
191
|
+
elif current_change and not current_block and not stripped.startswith('<'):
|
192
|
+
if stripped:
|
193
|
+
current_change.content += line + '\n'
|
194
|
+
|
195
|
+
return [c for c in changes if not self.has_invalid_tags]
|
196
|
+
|
197
|
+
except Exception as e:
|
198
|
+
self.console.print(f"[red]Error parsing XML: {str(e)}[/]")
|
199
|
+
self.console.print(f"[red]Error occurred at line {i + 1}:[/]")
|
200
|
+
self.console.print("\nOriginal response:")
|
201
|
+
self.console.print(response)
|
202
|
+
return []
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) [year] [fullname]
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,106 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: janito
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Language-Driven Software Development Assistant powered by Claude AI
|
5
|
+
Home-page: https://github.com/joaompinto/janito
|
6
|
+
Author: João M. Pinto
|
7
|
+
Author-email: "João M. Pinto" <lamego.pinto@gmail.com>
|
8
|
+
License: MIT
|
9
|
+
Project-URL: Homepage, https://github.com/joaompinto/janito
|
10
|
+
Project-URL: Documentation, https://github.com/joaompinto/janito#readme
|
11
|
+
Project-URL: Repository, https://github.com/joaompinto/janito.git
|
12
|
+
Keywords: ai,development,claude,assistant,code
|
13
|
+
Requires-Python: >=3.8
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
License-File: LICENSE
|
16
|
+
Requires-Dist: anthropic
|
17
|
+
Requires-Dist: prompt_toolkit
|
18
|
+
Requires-Dist: rich
|
19
|
+
Requires-Dist: typer
|
20
|
+
Requires-Dist: watchdog
|
21
|
+
Requires-Dist: pytest
|
22
|
+
|
23
|
+
|
24
|
+
Janito is an open source Language-Driven Software Development Assistant powered by Claude AI. It helps developers understand, modify, and improve their Python code through natural language interaction.
|
25
|
+
|
26
|
+
## Features
|
27
|
+
|
28
|
+
- Natural language code interactions
|
29
|
+
- File system monitoring and auto-restart
|
30
|
+
- Interactive command-line interface
|
31
|
+
- Workspace management and visualization
|
32
|
+
- History and session management
|
33
|
+
- Debug mode for troubleshooting
|
34
|
+
- Syntax error detection and fixing
|
35
|
+
- Python file execution
|
36
|
+
- File editing with system editor
|
37
|
+
|
38
|
+
## Installation
|
39
|
+
|
40
|
+
```bash
|
41
|
+
# Install package
|
42
|
+
pip install janito
|
43
|
+
```
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
Start Janito in your project directory:
|
48
|
+
|
49
|
+
```bash
|
50
|
+
python -m janito
|
51
|
+
```
|
52
|
+
|
53
|
+
Or launch with options:
|
54
|
+
|
55
|
+
```bash
|
56
|
+
python -m janito --debug # Enable debug mode
|
57
|
+
python -m janito --no-watch # Disable file watching
|
58
|
+
```
|
59
|
+
|
60
|
+
### Commands
|
61
|
+
|
62
|
+
- `.help` - Show help information
|
63
|
+
- `.exit` - Exit the console
|
64
|
+
- `.clear` - Clear console output
|
65
|
+
- `.debug` - Toggle debug mode
|
66
|
+
- `.workspace` - Show workspace structure
|
67
|
+
- `.last` - Show last Claude response
|
68
|
+
- `.show <file>` - Show file content with syntax highlighting
|
69
|
+
- `.check` - Check workspace Python files for syntax errors
|
70
|
+
- `.p <file>` - Run a Python file
|
71
|
+
- `.python <file>` - Run a Python file (alias for .p)
|
72
|
+
- `.edit <file>` - Open file in system editor
|
73
|
+
|
74
|
+
### Input Formats
|
75
|
+
|
76
|
+
- `!request` - Request file changes (e.g. '!add logging to utils.py')
|
77
|
+
- `request?` - Get information and analysis without changes
|
78
|
+
- `request` - General discussion and queries
|
79
|
+
- `$command` - Execute shell commands
|
80
|
+
|
81
|
+
## Configuration
|
82
|
+
|
83
|
+
Requires an Anthropic API key set via environment variable:
|
84
|
+
|
85
|
+
```bash
|
86
|
+
export ANTHROPIC_API_KEY='your_api_key_here'
|
87
|
+
```
|
88
|
+
|
89
|
+
## Development
|
90
|
+
|
91
|
+
The package consists of several modules:
|
92
|
+
|
93
|
+
- `janito.py` - Core functionality and CLI interface
|
94
|
+
- `change.py` - File modification and change tracking
|
95
|
+
- `claude.py` - Claude API interaction
|
96
|
+
- `console.py` - Interactive console and command handling
|
97
|
+
- `commands.py` - Command implementations
|
98
|
+
- `prompts.py` - Prompt templates and builders
|
99
|
+
- `watcher.py` - File system monitoring
|
100
|
+
- `workspace.py` - Workspace analysis and management
|
101
|
+
- `xmlchangeparser.py` - XML parser for file changes
|
102
|
+
- `watcher.py` - File system monitoring
|
103
|
+
|
104
|
+
## License
|
105
|
+
|
106
|
+
MIT License
|
@@ -0,0 +1,19 @@
|
|
1
|
+
janito/__init__.py,sha256=y6hOI0whYLZTk73Uoq-dxcCGKdPmAH9Fa5ywXV4CIaM,87
|
2
|
+
janito/__main__.py,sha256=e_8IHo0JtPt8lgS43UTUzkaIu47SZ8PjhOomNqrt6Q4,173
|
3
|
+
janito/change.py,sha256=4GueUiqKx6fqzmwaxTyj9n-rifyc54IUdoxmJHxnnBc,17898
|
4
|
+
janito/claude.py,sha256=KFyhAqRKY0iUMiuazlpvPlR6uufJCkkxZx_Jxeqdu1w,4294
|
5
|
+
janito/commands.py,sha256=CqP0oOA9Rl6siQT0mNo3jeB7ubZwSCxsmXLTA0ostAM,15305
|
6
|
+
janito/console.py,sha256=-SqCPFBt-7gqROCWlkGUcmOAJGC1_YnS5cuwXZITHAk,14909
|
7
|
+
janito/janito.py,sha256=bgddRXQNQj28scRYNhXPeA3T3Jh84A9EOZApZ_OD0us,13311
|
8
|
+
janito/prompts.py,sha256=4TkFnam7Ya8sBUQ9k5iAt72Ca7-k9KPmKjaqilfCX6U,5879
|
9
|
+
janito/watcher.py,sha256=8icD9XnHnYpy_XI_i5Dg6tHpw27ecJie2ZqpEo66COY,3363
|
10
|
+
janito/workspace.py,sha256=do66QRQ2VIpMofJuyVUyS_Gp5WMDG-5XoNYm270Zv0s,6979
|
11
|
+
janito/xmlchangeparser.py,sha256=kfMa9AntsPBhG_R2NHFU3DZsaRh5BXRREZCnCfR1SDs,8576
|
12
|
+
tests/__init__.py,sha256=EjWgRAmKaumanB8t00SWGaTu2b7AuZNDiMxqYoAhVJI,26
|
13
|
+
tests/conftest.py,sha256=_8lI6aTT2s1A14ggojPW-bI0imOj1JmXwdR1_2BPuCw,185
|
14
|
+
tests/test_change.py,sha256=wqufymSOo4ypMorMpQk9oyJYeYgO6Zn1H2GhU1Wx2ls,12482
|
15
|
+
janito-0.1.0.dist-info/LICENSE,sha256=pAZXnNE2dxxwXFIduGyn1gpvPefJtUYOYZOi3yeGG94,1068
|
16
|
+
janito-0.1.0.dist-info/METADATA,sha256=ROEPyzYWR3q8ibD4bpLGQGTyQBGEkpRx0IyKxVFHfc0,2979
|
17
|
+
janito-0.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
18
|
+
janito-0.1.0.dist-info/top_level.txt,sha256=b9vGz4twQlT5Sx2aD5wtd9NbXg3N40FjXgGRpcbKGVQ,13
|
19
|
+
janito-0.1.0.dist-info/RECORD,,
|
tests/__init__.py
ADDED