ai-coding-assistant 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.
Files changed (89) hide show
  1. ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
  2. ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
  3. ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
  4. ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
  5. ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
  6. coding_assistant/__init__.py +3 -0
  7. coding_assistant/__main__.py +19 -0
  8. coding_assistant/cli/__init__.py +1 -0
  9. coding_assistant/cli/app.py +158 -0
  10. coding_assistant/cli/commands/__init__.py +19 -0
  11. coding_assistant/cli/commands/ask.py +178 -0
  12. coding_assistant/cli/commands/config.py +438 -0
  13. coding_assistant/cli/commands/diagram.py +267 -0
  14. coding_assistant/cli/commands/document.py +410 -0
  15. coding_assistant/cli/commands/explain.py +192 -0
  16. coding_assistant/cli/commands/fix.py +249 -0
  17. coding_assistant/cli/commands/index.py +162 -0
  18. coding_assistant/cli/commands/refactor.py +245 -0
  19. coding_assistant/cli/commands/search.py +182 -0
  20. coding_assistant/cli/commands/serve_docs.py +128 -0
  21. coding_assistant/cli/repl.py +381 -0
  22. coding_assistant/cli/theme.py +90 -0
  23. coding_assistant/codebase/__init__.py +1 -0
  24. coding_assistant/codebase/crawler.py +93 -0
  25. coding_assistant/codebase/parser.py +266 -0
  26. coding_assistant/config/__init__.py +25 -0
  27. coding_assistant/config/config_manager.py +615 -0
  28. coding_assistant/config/settings.py +82 -0
  29. coding_assistant/context/__init__.py +19 -0
  30. coding_assistant/context/chunker.py +443 -0
  31. coding_assistant/context/enhanced_retriever.py +322 -0
  32. coding_assistant/context/hybrid_search.py +311 -0
  33. coding_assistant/context/ranker.py +355 -0
  34. coding_assistant/context/retriever.py +119 -0
  35. coding_assistant/context/window.py +362 -0
  36. coding_assistant/documentation/__init__.py +23 -0
  37. coding_assistant/documentation/agents/__init__.py +27 -0
  38. coding_assistant/documentation/agents/coordinator.py +510 -0
  39. coding_assistant/documentation/agents/module_documenter.py +111 -0
  40. coding_assistant/documentation/agents/synthesizer.py +139 -0
  41. coding_assistant/documentation/agents/task_delegator.py +100 -0
  42. coding_assistant/documentation/decomposition/__init__.py +21 -0
  43. coding_assistant/documentation/decomposition/context_preserver.py +477 -0
  44. coding_assistant/documentation/decomposition/module_detector.py +302 -0
  45. coding_assistant/documentation/decomposition/partitioner.py +621 -0
  46. coding_assistant/documentation/generators/__init__.py +14 -0
  47. coding_assistant/documentation/generators/dataflow_generator.py +440 -0
  48. coding_assistant/documentation/generators/diagram_generator.py +511 -0
  49. coding_assistant/documentation/graph/__init__.py +13 -0
  50. coding_assistant/documentation/graph/dependency_builder.py +468 -0
  51. coding_assistant/documentation/graph/module_analyzer.py +475 -0
  52. coding_assistant/documentation/writers/__init__.py +11 -0
  53. coding_assistant/documentation/writers/markdown_writer.py +322 -0
  54. coding_assistant/embeddings/__init__.py +0 -0
  55. coding_assistant/embeddings/generator.py +89 -0
  56. coding_assistant/embeddings/store.py +187 -0
  57. coding_assistant/exceptions/__init__.py +50 -0
  58. coding_assistant/exceptions/base.py +110 -0
  59. coding_assistant/exceptions/llm.py +249 -0
  60. coding_assistant/exceptions/recovery.py +263 -0
  61. coding_assistant/exceptions/storage.py +213 -0
  62. coding_assistant/exceptions/validation.py +230 -0
  63. coding_assistant/llm/__init__.py +1 -0
  64. coding_assistant/llm/client.py +277 -0
  65. coding_assistant/llm/gemini_client.py +181 -0
  66. coding_assistant/llm/groq_client.py +160 -0
  67. coding_assistant/llm/prompts.py +98 -0
  68. coding_assistant/llm/together_client.py +160 -0
  69. coding_assistant/operations/__init__.py +13 -0
  70. coding_assistant/operations/differ.py +369 -0
  71. coding_assistant/operations/generator.py +347 -0
  72. coding_assistant/operations/linter.py +430 -0
  73. coding_assistant/operations/validator.py +406 -0
  74. coding_assistant/storage/__init__.py +9 -0
  75. coding_assistant/storage/database.py +363 -0
  76. coding_assistant/storage/session.py +231 -0
  77. coding_assistant/utils/__init__.py +31 -0
  78. coding_assistant/utils/cache.py +477 -0
  79. coding_assistant/utils/hardware.py +132 -0
  80. coding_assistant/utils/keystore.py +206 -0
  81. coding_assistant/utils/logger.py +32 -0
  82. coding_assistant/utils/progress.py +311 -0
  83. coding_assistant/validation/__init__.py +13 -0
  84. coding_assistant/validation/files.py +305 -0
  85. coding_assistant/validation/inputs.py +335 -0
  86. coding_assistant/validation/params.py +280 -0
  87. coding_assistant/validation/sanitizers.py +243 -0
  88. coding_assistant/vcs/__init__.py +5 -0
  89. coding_assistant/vcs/git.py +269 -0
@@ -0,0 +1,305 @@
1
+ """File path validation."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional, List
6
+
7
+ from coding_assistant.exceptions.validation import (
8
+ ValidationError,
9
+ FileNotFoundError,
10
+ PathSecurityError
11
+ )
12
+
13
+
14
+ class FileValidator:
15
+ """
16
+ Validates file paths for safety and existence.
17
+
18
+ Prevents path traversal and other security issues.
19
+ """
20
+
21
+ # Dangerous path patterns
22
+ DANGEROUS_PATTERNS = [
23
+ '..', # Path traversal
24
+ '~/', # Home directory
25
+ '/etc', # System config
26
+ '/sys', # System files
27
+ '/proc', # Process info
28
+ '/dev', # Devices
29
+ ]
30
+
31
+ @staticmethod
32
+ def validate_file_path(
33
+ path: str,
34
+ must_exist: bool = False,
35
+ must_be_file: bool = False,
36
+ must_be_dir: bool = False,
37
+ allowed_extensions: Optional[List[str]] = None,
38
+ base_dir: Optional[Path] = None
39
+ ) -> Path:
40
+ """
41
+ Validate file path for safety and correctness.
42
+
43
+ Args:
44
+ path: File path to validate
45
+ must_exist: Whether file must exist
46
+ must_be_file: Whether path must be a file (not directory)
47
+ must_be_dir: Whether path must be a directory
48
+ allowed_extensions: List of allowed file extensions (e.g., ['.py', '.js'])
49
+ base_dir: Base directory to restrict paths to
50
+
51
+ Returns:
52
+ Validated Path object
53
+
54
+ Raises:
55
+ ValidationError: If path is invalid
56
+ FileNotFoundError: If file doesn't exist when required
57
+ PathSecurityError: If path is potentially dangerous
58
+ """
59
+ if not path:
60
+ raise ValidationError(
61
+ "file path",
62
+ "Path cannot be empty"
63
+ )
64
+
65
+ # Convert to Path
66
+ try:
67
+ file_path = Path(path)
68
+ except Exception as e:
69
+ raise ValidationError(
70
+ "file path",
71
+ f"Invalid path format: {e}",
72
+ value=path
73
+ )
74
+
75
+ # Security checks
76
+ FileValidator._check_path_security(file_path)
77
+
78
+ # Resolve to absolute path
79
+ try:
80
+ file_path = file_path.resolve()
81
+ except Exception as e:
82
+ raise ValidationError(
83
+ "file path",
84
+ f"Cannot resolve path: {e}",
85
+ value=path
86
+ )
87
+
88
+ # Check if within base directory
89
+ if base_dir:
90
+ base_dir = base_dir.resolve()
91
+ try:
92
+ file_path.relative_to(base_dir)
93
+ except ValueError:
94
+ raise PathSecurityError(
95
+ file_path,
96
+ f"Path is outside allowed directory: {base_dir}"
97
+ )
98
+
99
+ # Check existence
100
+ if must_exist and not file_path.exists():
101
+ raise FileNotFoundError(file_path)
102
+
103
+ # Check file vs directory
104
+ if file_path.exists():
105
+ if must_be_file and not file_path.is_file():
106
+ raise ValidationError(
107
+ "file path",
108
+ f"Path is not a file: {file_path}",
109
+ value=str(file_path)
110
+ )
111
+
112
+ if must_be_dir and not file_path.is_dir():
113
+ raise ValidationError(
114
+ "file path",
115
+ f"Path is not a directory: {file_path}",
116
+ value=str(file_path)
117
+ )
118
+
119
+ # Check extension
120
+ if allowed_extensions and file_path.suffix.lower() not in allowed_extensions:
121
+ raise ValidationError(
122
+ "file path",
123
+ f"File type not allowed. Allowed: {', '.join(allowed_extensions)}",
124
+ value=str(file_path),
125
+ suggestion=f"Use a file with one of these extensions: {', '.join(allowed_extensions)}"
126
+ )
127
+
128
+ return file_path
129
+
130
+ @staticmethod
131
+ def _check_path_security(file_path: Path) -> None:
132
+ """
133
+ Check path for security issues.
134
+
135
+ Args:
136
+ file_path: Path to check
137
+
138
+ Raises:
139
+ PathSecurityError: If path is dangerous
140
+ """
141
+ path_str = str(file_path)
142
+
143
+ # Check for dangerous patterns
144
+ for pattern in FileValidator.DANGEROUS_PATTERNS:
145
+ if pattern in path_str:
146
+ # Allow .. only in resolved paths that don't escape
147
+ if pattern == '..' and not path_str.startswith('..'):
148
+ continue
149
+
150
+ raise PathSecurityError(
151
+ file_path,
152
+ f"Path contains dangerous pattern: {pattern}"
153
+ )
154
+
155
+ # Check for absolute paths to system directories
156
+ if file_path.is_absolute():
157
+ parts = file_path.parts
158
+ if len(parts) > 1 and parts[1] in ['etc', 'sys', 'proc', 'dev', 'boot']:
159
+ raise PathSecurityError(
160
+ file_path,
161
+ "Access to system directories not allowed"
162
+ )
163
+
164
+ @staticmethod
165
+ def validate_project_path(path: str) -> Path:
166
+ """
167
+ Validate project root path.
168
+
169
+ Args:
170
+ path: Project path
171
+
172
+ Returns:
173
+ Validated Path object
174
+
175
+ Raises:
176
+ ValidationError: If path is invalid
177
+ """
178
+ project_path = FileValidator.validate_file_path(
179
+ path,
180
+ must_exist=True,
181
+ must_be_dir=True
182
+ )
183
+
184
+ # Check if it looks like a code project
185
+ # (has common files like .git, src/, package.json, etc.)
186
+ indicators = [
187
+ '.git',
188
+ 'src',
189
+ 'lib',
190
+ 'package.json',
191
+ 'requirements.txt',
192
+ 'pyproject.toml',
193
+ 'Cargo.toml',
194
+ 'go.mod',
195
+ '.gitignore'
196
+ ]
197
+
198
+ has_indicator = any(
199
+ (project_path / indicator).exists()
200
+ for indicator in indicators
201
+ )
202
+
203
+ if not has_indicator:
204
+ # Just a warning, not an error
205
+ pass # Could add warning log
206
+
207
+ return project_path
208
+
209
+ @staticmethod
210
+ def validate_output_path(
211
+ path: str,
212
+ allow_overwrite: bool = False
213
+ ) -> Path:
214
+ """
215
+ Validate output file path.
216
+
217
+ Args:
218
+ path: Output file path
219
+ allow_overwrite: Whether to allow overwriting existing files
220
+
221
+ Returns:
222
+ Validated Path object
223
+
224
+ Raises:
225
+ ValidationError: If path is invalid or file exists
226
+ """
227
+ output_path = FileValidator.validate_file_path(path)
228
+
229
+ # Check if file exists
230
+ if output_path.exists() and not allow_overwrite:
231
+ raise ValidationError(
232
+ "output path",
233
+ "File already exists",
234
+ value=str(output_path),
235
+ suggestion="Use a different path or enable overwrite"
236
+ )
237
+
238
+ # Check if parent directory exists and is writable
239
+ parent = output_path.parent
240
+ if not parent.exists():
241
+ raise ValidationError(
242
+ "output path",
243
+ f"Parent directory does not exist: {parent}",
244
+ value=str(output_path),
245
+ suggestion=f"Create directory first: mkdir -p {parent}"
246
+ )
247
+
248
+ if not os.access(parent, os.W_OK):
249
+ raise ValidationError(
250
+ "output path",
251
+ f"Parent directory is not writable: {parent}",
252
+ value=str(output_path)
253
+ )
254
+
255
+ return output_path
256
+
257
+ @staticmethod
258
+ def is_code_file(file_path: Path) -> bool:
259
+ """
260
+ Check if file is a code file.
261
+
262
+ Args:
263
+ file_path: File path to check
264
+
265
+ Returns:
266
+ True if file appears to be code
267
+ """
268
+ code_extensions = {
269
+ '.py', '.js', '.ts', '.jsx', '.tsx',
270
+ '.java', '.c', '.cpp', '.h', '.hpp',
271
+ '.cs', '.go', '.rs', '.rb', '.php',
272
+ '.swift', '.kt', '.scala', '.R',
273
+ '.sql', '.sh', '.bash', '.zsh',
274
+ '.html', '.css', '.scss', '.less',
275
+ '.json', '.yaml', '.yml', '.toml',
276
+ '.xml', '.md', '.rst'
277
+ }
278
+
279
+ return file_path.suffix.lower() in code_extensions
280
+
281
+ @staticmethod
282
+ def get_relative_path(
283
+ file_path: Path,
284
+ base_path: Path
285
+ ) -> Path:
286
+ """
287
+ Get relative path from base.
288
+
289
+ Args:
290
+ file_path: File path
291
+ base_path: Base path
292
+
293
+ Returns:
294
+ Relative path
295
+
296
+ Raises:
297
+ ValidationError: If file_path is not under base_path
298
+ """
299
+ try:
300
+ return file_path.relative_to(base_path)
301
+ except ValueError:
302
+ raise ValidationError(
303
+ "file path",
304
+ f"Path {file_path} is not under {base_path}"
305
+ )
@@ -0,0 +1,335 @@
1
+ """Input validation utilities."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional, List
6
+
7
+ from coding_assistant.exceptions.validation import (
8
+ ValidationError,
9
+ InvalidQueryError,
10
+ InvalidParameterError,
11
+ EmptyInputError
12
+ )
13
+
14
+
15
+ class InputValidator:
16
+ """
17
+ Validates user inputs for safety and correctness.
18
+
19
+ Provides methods for validating queries, parameters, and other inputs.
20
+ """
21
+
22
+ # Constants
23
+ MIN_QUERY_LENGTH = 3
24
+ MAX_QUERY_LENGTH = 500
25
+ MAX_FILE_PATH_LENGTH = 4096
26
+
27
+ @staticmethod
28
+ def validate_query(
29
+ query: str,
30
+ min_length: Optional[int] = None,
31
+ max_length: Optional[int] = None
32
+ ) -> str:
33
+ """
34
+ Validate search/ask query.
35
+
36
+ Args:
37
+ query: User query
38
+ min_length: Minimum query length (default: 3)
39
+ max_length: Maximum query length (default: 500)
40
+
41
+ Returns:
42
+ Cleaned query string
43
+
44
+ Raises:
45
+ InvalidQueryError: If query is invalid
46
+ EmptyInputError: If query is empty
47
+ """
48
+ min_length = min_length or InputValidator.MIN_QUERY_LENGTH
49
+ max_length = max_length or InputValidator.MAX_QUERY_LENGTH
50
+
51
+ # Check for None or empty
52
+ if query is None:
53
+ raise EmptyInputError("query")
54
+
55
+ # Strip whitespace
56
+ query = query.strip()
57
+
58
+ if not query:
59
+ raise EmptyInputError("query")
60
+
61
+ # Check length
62
+ if len(query) < min_length:
63
+ raise InvalidQueryError(
64
+ query,
65
+ f"Query too short (minimum {min_length} characters)"
66
+ )
67
+
68
+ if len(query) > max_length:
69
+ raise InvalidQueryError(
70
+ query,
71
+ f"Query too long (maximum {max_length} characters)"
72
+ )
73
+
74
+ # Check for suspicious patterns (injection attempts)
75
+ suspicious_patterns = [
76
+ r'<script', # XSS
77
+ r'javascript:', # XSS
78
+ r'\$\{', # Template injection
79
+ r'`[^`]*`', # Command substitution
80
+ ]
81
+
82
+ for pattern in suspicious_patterns:
83
+ if re.search(pattern, query, re.IGNORECASE):
84
+ raise InvalidQueryError(
85
+ query,
86
+ "Query contains suspicious patterns"
87
+ )
88
+
89
+ return query
90
+
91
+ @staticmethod
92
+ def validate_positive_int(
93
+ value: any,
94
+ name: str,
95
+ min_value: int = 1,
96
+ max_value: Optional[int] = None
97
+ ) -> int:
98
+ """
99
+ Validate positive integer parameter.
100
+
101
+ Args:
102
+ value: Value to validate
103
+ name: Parameter name
104
+ min_value: Minimum value (default: 1)
105
+ max_value: Maximum value (optional)
106
+
107
+ Returns:
108
+ Validated integer
109
+
110
+ Raises:
111
+ InvalidParameterError: If value is invalid
112
+ """
113
+ # Check type
114
+ if not isinstance(value, int):
115
+ raise InvalidParameterError(
116
+ name,
117
+ value,
118
+ "positive integer"
119
+ )
120
+
121
+ # Check positive
122
+ if value < min_value:
123
+ raise InvalidParameterError(
124
+ name,
125
+ value,
126
+ f"positive integer >= {min_value}"
127
+ )
128
+
129
+ # Check maximum
130
+ if max_value is not None and value > max_value:
131
+ raise InvalidParameterError(
132
+ name,
133
+ value,
134
+ f"positive integer <= {max_value}",
135
+ suggestion=f"Use a value between {min_value} and {max_value}"
136
+ )
137
+
138
+ return value
139
+
140
+ @staticmethod
141
+ def validate_float_range(
142
+ value: any,
143
+ name: str,
144
+ min_value: float = 0.0,
145
+ max_value: float = 1.0
146
+ ) -> float:
147
+ """
148
+ Validate float in range.
149
+
150
+ Args:
151
+ value: Value to validate
152
+ name: Parameter name
153
+ min_value: Minimum value
154
+ max_value: Maximum value
155
+
156
+ Returns:
157
+ Validated float
158
+
159
+ Raises:
160
+ InvalidParameterError: If value is invalid
161
+ """
162
+ # Check type
163
+ if not isinstance(value, (int, float)):
164
+ raise InvalidParameterError(
165
+ name,
166
+ value,
167
+ f"number between {min_value} and {max_value}"
168
+ )
169
+
170
+ value = float(value)
171
+
172
+ # Check range
173
+ if value < min_value or value > max_value:
174
+ raise InvalidParameterError(
175
+ name,
176
+ value,
177
+ f"number between {min_value} and {max_value}"
178
+ )
179
+
180
+ return value
181
+
182
+ @staticmethod
183
+ def validate_choice(
184
+ value: str,
185
+ name: str,
186
+ choices: List[str],
187
+ case_sensitive: bool = False
188
+ ) -> str:
189
+ """
190
+ Validate value is in list of choices.
191
+
192
+ Args:
193
+ value: Value to validate
194
+ name: Parameter name
195
+ choices: List of valid choices
196
+ case_sensitive: Whether to enforce case sensitivity
197
+
198
+ Returns:
199
+ Validated choice
200
+
201
+ Raises:
202
+ InvalidParameterError: If value not in choices
203
+ """
204
+ if not value:
205
+ raise EmptyInputError(name)
206
+
207
+ # Normalize case if not case-sensitive
208
+ if not case_sensitive:
209
+ value_lower = value.lower()
210
+ choices_map = {c.lower(): c for c in choices}
211
+
212
+ if value_lower in choices_map:
213
+ return choices_map[value_lower]
214
+ else:
215
+ if value in choices:
216
+ return value
217
+
218
+ # Not found
219
+ raise InvalidParameterError(
220
+ name,
221
+ value,
222
+ f"one of: {', '.join(choices)}",
223
+ suggestion=f"Choose from: {', '.join(choices)}"
224
+ )
225
+
226
+ @staticmethod
227
+ def validate_boolean(value: any, name: str) -> bool:
228
+ """
229
+ Validate boolean parameter.
230
+
231
+ Args:
232
+ value: Value to validate
233
+ name: Parameter name
234
+
235
+ Returns:
236
+ Boolean value
237
+
238
+ Raises:
239
+ InvalidParameterError: If value is not boolean-like
240
+ """
241
+ if isinstance(value, bool):
242
+ return value
243
+
244
+ if isinstance(value, str):
245
+ value_lower = value.lower()
246
+
247
+ if value_lower in ('true', 'yes', '1', 'on', 'y'):
248
+ return True
249
+ elif value_lower in ('false', 'no', '0', 'off', 'n'):
250
+ return False
251
+
252
+ raise InvalidParameterError(
253
+ name,
254
+ value,
255
+ "boolean (true/false)",
256
+ suggestion="Use true/false, yes/no, or 1/0"
257
+ )
258
+
259
+ @staticmethod
260
+ def validate_non_empty(value: str, name: str) -> str:
261
+ """
262
+ Validate string is not empty.
263
+
264
+ Args:
265
+ value: Value to validate
266
+ name: Field name
267
+
268
+ Returns:
269
+ Non-empty string
270
+
271
+ Raises:
272
+ EmptyInputError: If value is empty
273
+ """
274
+ if not value or not value.strip():
275
+ raise EmptyInputError(name)
276
+
277
+ return value.strip()
278
+
279
+ @staticmethod
280
+ def validate_pattern(
281
+ value: str,
282
+ name: str,
283
+ pattern: str,
284
+ description: str
285
+ ) -> str:
286
+ """
287
+ Validate string matches regex pattern.
288
+
289
+ Args:
290
+ value: Value to validate
291
+ name: Field name
292
+ pattern: Regex pattern
293
+ description: Pattern description for error message
294
+
295
+ Returns:
296
+ Validated string
297
+
298
+ Raises:
299
+ ValidationError: If value doesn't match pattern
300
+ """
301
+ if not re.match(pattern, value):
302
+ raise ValidationError(
303
+ name,
304
+ f"Does not match expected format: {description}",
305
+ value=value
306
+ )
307
+
308
+ return value
309
+
310
+ @staticmethod
311
+ def sanitize_string(value: str, max_length: Optional[int] = None) -> str:
312
+ """
313
+ Sanitize string input.
314
+
315
+ Args:
316
+ value: String to sanitize
317
+ max_length: Maximum length (optional)
318
+
319
+ Returns:
320
+ Sanitized string
321
+ """
322
+ if not value:
323
+ return ""
324
+
325
+ # Strip whitespace
326
+ value = value.strip()
327
+
328
+ # Remove null bytes
329
+ value = value.replace('\x00', '')
330
+
331
+ # Truncate if needed
332
+ if max_length and len(value) > max_length:
333
+ value = value[:max_length]
334
+
335
+ return value