claude-dev-cli 0.12.0__tar.gz → 0.12.1__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.

Potentially problematic release.


This version of claude-dev-cli might be problematic. Click here for more details.

Files changed (47) hide show
  1. {claude_dev_cli-0.12.0/src/claude_dev_cli.egg-info → claude_dev_cli-0.12.1}/PKG-INFO +1 -1
  2. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/pyproject.toml +1 -1
  3. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/__init__.py +1 -1
  4. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/cli.py +8 -8
  5. claude_dev_cli-0.12.1/src/claude_dev_cli/multi_file_handler.py +348 -0
  6. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1/src/claude_dev_cli.egg-info}/PKG-INFO +1 -1
  7. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli.egg-info/SOURCES.txt +1 -0
  8. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/LICENSE +0 -0
  9. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/MANIFEST.in +0 -0
  10. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/README.md +0 -0
  11. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/setup.cfg +0 -0
  12. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/commands.py +0 -0
  13. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/config.py +0 -0
  14. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/context.py +0 -0
  15. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/core.py +0 -0
  16. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/history.py +0 -0
  17. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/input_sources.py +0 -0
  18. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/path_utils.py +0 -0
  19. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/plugins/__init__.py +0 -0
  20. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/plugins/base.py +0 -0
  21. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/plugins/diff_editor/__init__.py +0 -0
  22. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/plugins/diff_editor/plugin.py +0 -0
  23. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/plugins/diff_editor/viewer.py +0 -0
  24. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/secure_storage.py +0 -0
  25. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/template_manager.py +0 -0
  26. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/templates.py +0 -0
  27. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/toon_utils.py +0 -0
  28. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/usage.py +0 -0
  29. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/warp_integration.py +0 -0
  30. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli/workflows.py +0 -0
  31. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli.egg-info/dependency_links.txt +0 -0
  32. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli.egg-info/entry_points.txt +0 -0
  33. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli.egg-info/requires.txt +0 -0
  34. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/src/claude_dev_cli.egg-info/top_level.txt +0 -0
  35. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_cli.py +0 -0
  36. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_commands.py +0 -0
  37. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_config.py +0 -0
  38. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_context.py +0 -0
  39. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_core.py +0 -0
  40. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_diff_editor.py +0 -0
  41. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_history.py +0 -0
  42. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_input_sources.py +0 -0
  43. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_path_utils.py +0 -0
  44. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_secure_storage.py +0 -0
  45. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_template_manager.py +0 -0
  46. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_toon_utils.py +0 -0
  47. {claude_dev_cli-0.12.0 → claude_dev_cli-0.12.1}/tests/test_usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.12.0
3
+ Version: 0.12.1
4
4
  Summary: A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking
5
5
  Author-email: Julio <thinmanj@users.noreply.github.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "claude-dev-cli"
7
- version = "0.12.0"
7
+ version = "0.12.1"
8
8
  description = "A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.12.0"
12
+ __version__ = "0.12.1"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
@@ -1157,15 +1157,15 @@ def gen_code(
1157
1157
 
1158
1158
  # Generate code
1159
1159
  with console.status(f"[bold blue]Generating code..."):
1160
- client = ClaudeClient(api_config_name=api, model=model)
1161
- result = client.call(prompt)
1160
+ client = ClaudeClient(api_config_name=api)
1161
+ result = client.call(prompt, model=model)
1162
1162
 
1163
1163
  # Interactive refinement
1164
1164
  if interactive:
1165
1165
  console.print("\n[bold]Initial Code:[/bold]\n")
1166
1166
  console.print(result)
1167
1167
 
1168
- client = ClaudeClient(api_config_name=api, model=model)
1168
+ client = ClaudeClient(api_config_name=api)
1169
1169
  conversation_context = [result]
1170
1170
 
1171
1171
  while True:
@@ -1188,7 +1188,7 @@ def gen_code(
1188
1188
 
1189
1189
  console.print("\n[bold green]Claude:[/bold green] ", end='')
1190
1190
  response_parts = []
1191
- for chunk in client.call_streaming(refinement_prompt):
1191
+ for chunk in client.call_streaming(refinement_prompt, model=model):
1192
1192
  console.print(chunk, end='')
1193
1193
  response_parts.append(chunk)
1194
1194
  console.print()
@@ -1314,8 +1314,8 @@ def gen_feature(
1314
1314
 
1315
1315
  # Generate feature implementation
1316
1316
  with console.status(f"[bold blue]Analyzing codebase and generating feature implementation..."):
1317
- client = ClaudeClient(api_config_name=api, model=model)
1318
- result = client.call(prompt)
1317
+ client = ClaudeClient(api_config_name=api)
1318
+ result = client.call(prompt, model=model)
1319
1319
 
1320
1320
  # Show result
1321
1321
  from rich.markdown import Markdown
@@ -1329,7 +1329,7 @@ def gen_feature(
1329
1329
 
1330
1330
  # Interactive refinement
1331
1331
  if interactive:
1332
- client = ClaudeClient(api_config_name=api, model=model)
1332
+ client = ClaudeClient(api_config_name=api)
1333
1333
  conversation_context = [result]
1334
1334
 
1335
1335
  while True:
@@ -1353,7 +1353,7 @@ def gen_feature(
1353
1353
 
1354
1354
  console.print("\n[bold green]Claude:[/bold green] ", end='')
1355
1355
  response_parts = []
1356
- for chunk in client.call_streaming(refinement_prompt):
1356
+ for chunk in client.call_streaming(refinement_prompt, model=model):
1357
1357
  console.print(chunk, end='')
1358
1358
  response_parts.append(chunk)
1359
1359
  console.print()
@@ -0,0 +1,348 @@
1
+ """Multi-file handler for parsing and applying AI-generated file changes."""
2
+
3
+ import re
4
+ import difflib
5
+ from pathlib import Path
6
+ from typing import List, Tuple, Optional, Literal
7
+ from dataclasses import dataclass
8
+ from rich.console import Console
9
+ from rich.tree import Tree
10
+ from rich.panel import Panel
11
+
12
+
13
+ @dataclass
14
+ class FileChange:
15
+ """Represents a single file change."""
16
+ path: str
17
+ content: str
18
+ change_type: Literal["create", "modify", "delete"]
19
+ original_content: Optional[str] = None
20
+
21
+ @property
22
+ def line_count(self) -> int:
23
+ """Count lines in content."""
24
+ return len(self.content.splitlines()) if self.content else 0
25
+
26
+ @property
27
+ def diff(self) -> Optional[str]:
28
+ """Generate unified diff for modifications."""
29
+ if self.change_type != "modify" or not self.original_content:
30
+ return None
31
+
32
+ original_lines = self.original_content.splitlines(keepends=True)
33
+ new_lines = self.content.splitlines(keepends=True)
34
+
35
+ diff_lines = list(difflib.unified_diff(
36
+ original_lines,
37
+ new_lines,
38
+ fromfile=f"a/{self.path}",
39
+ tofile=f"b/{self.path}",
40
+ lineterm=''
41
+ ))
42
+
43
+ return ''.join(diff_lines) if diff_lines else None
44
+
45
+
46
+ class MultiFileResponse:
47
+ """Parses and handles multi-file AI responses."""
48
+
49
+ def __init__(self):
50
+ self.files: List[FileChange] = []
51
+
52
+ def parse_response(self, text: str, base_path: Optional[Path] = None) -> None:
53
+ """Parse AI response to extract file changes.
54
+
55
+ Supports formats:
56
+ - ## File: path/to/file.ext
57
+ - ## Create: path/to/file.ext
58
+ - ## Modify: path/to/file.ext
59
+ - ## Delete: path/to/file.ext
60
+
61
+ Args:
62
+ text: AI response text
63
+ base_path: Base directory to check for existing files (for modifications)
64
+ """
65
+ self.files = []
66
+
67
+ # Pattern to match file markers and code blocks
68
+ # Matches: ## File: path or ## Create: path or ## Modify: path or ## Delete: path
69
+ file_pattern = r'^##\s+(File|Create|Modify|Delete):\s*(.+?)$'
70
+ code_block_pattern = r'```(\w+)?\n(.*?)```'
71
+
72
+ lines = text.split('\n')
73
+ i = 0
74
+
75
+ while i < len(lines):
76
+ line = lines[i].strip()
77
+ match = re.match(file_pattern, line, re.IGNORECASE)
78
+
79
+ if match:
80
+ action = match.group(1).lower()
81
+ file_path = match.group(2).strip()
82
+
83
+ # Map actions to change types
84
+ if action in ('file', 'create'):
85
+ change_type = 'create'
86
+ elif action == 'modify':
87
+ change_type = 'modify'
88
+ elif action == 'delete':
89
+ change_type = 'delete'
90
+ else:
91
+ change_type = 'create'
92
+
93
+ # For delete, no content needed
94
+ if change_type == 'delete':
95
+ self.files.append(FileChange(
96
+ path=file_path,
97
+ content='',
98
+ change_type='delete'
99
+ ))
100
+ i += 1
101
+ continue
102
+
103
+ # Extract code block following the file marker
104
+ remaining_text = '\n'.join(lines[i+1:])
105
+ code_match = re.search(code_block_pattern, remaining_text, re.DOTALL)
106
+
107
+ if code_match:
108
+ content = code_match.group(2).strip()
109
+
110
+ # Check if file exists for modifications
111
+ original_content = None
112
+ if change_type == 'modify' and base_path:
113
+ file_full_path = base_path / file_path
114
+ if file_full_path.exists():
115
+ original_content = file_full_path.read_text()
116
+ change_type = 'modify'
117
+ else:
118
+ # File doesn't exist, treat as create
119
+ change_type = 'create'
120
+
121
+ self.files.append(FileChange(
122
+ path=file_path,
123
+ content=content,
124
+ change_type=change_type,
125
+ original_content=original_content
126
+ ))
127
+
128
+ # Skip past the code block
129
+ lines_used = code_match.group(0).count('\n')
130
+ i += lines_used + 1
131
+ else:
132
+ # No code block found, skip this line
133
+ i += 1
134
+ else:
135
+ i += 1
136
+
137
+ def validate_paths(self, base_path: Path) -> List[str]:
138
+ """Validate file paths for security.
139
+
140
+ Returns list of validation errors, empty if all valid.
141
+ """
142
+ errors = []
143
+ base_path = base_path.resolve()
144
+
145
+ for file_change in self.files:
146
+ # Check for absolute paths
147
+ if Path(file_change.path).is_absolute():
148
+ errors.append(f"Absolute path not allowed: {file_change.path}")
149
+ continue
150
+
151
+ # Resolve the path
152
+ try:
153
+ full_path = (base_path / file_change.path).resolve()
154
+
155
+ # Check if resolved path is within base_path
156
+ if not str(full_path).startswith(str(base_path)):
157
+ errors.append(f"Path traversal detected: {file_change.path}")
158
+
159
+ except Exception as e:
160
+ errors.append(f"Invalid path {file_change.path}: {str(e)}")
161
+
162
+ return errors
163
+
164
+ def build_tree(self, base_path: Path) -> str:
165
+ """Generate visual directory tree.
166
+
167
+ Returns formatted tree string with colors and status indicators.
168
+ """
169
+ if not self.files:
170
+ return "No files"
171
+
172
+ # Build tree structure
173
+ tree = Tree(f"[bold cyan]{base_path.name or base_path}/[/bold cyan]")
174
+
175
+ # Group files by directory
176
+ dirs = {}
177
+ for file_change in self.files:
178
+ parts = Path(file_change.path).parts
179
+ current = dirs
180
+
181
+ for i, part in enumerate(parts[:-1]):
182
+ if part not in current:
183
+ current[part] = {}
184
+ current = current[part]
185
+
186
+ # Store file info at leaf
187
+ filename = parts[-1]
188
+ current[filename] = file_change
189
+
190
+ def add_to_tree(node, items):
191
+ """Recursively add items to tree."""
192
+ for name, value in sorted(items.items()):
193
+ if isinstance(value, dict):
194
+ # Directory
195
+ branch = node.add(f"[bold blue]{name}/[/bold blue]")
196
+ add_to_tree(branch, value)
197
+ elif isinstance(value, FileChange):
198
+ # File
199
+ if value.change_type == 'create':
200
+ status = "[green](new)[/green]"
201
+ elif value.change_type == 'modify':
202
+ status = "[yellow](modified)[/yellow]"
203
+ elif value.change_type == 'delete':
204
+ status = "[red](deleted)[/red]"
205
+ else:
206
+ status = ""
207
+
208
+ lines = f"{value.line_count} lines" if value.line_count > 0 else ""
209
+ node.add(f"{name} {status} [dim]{lines}[/dim]")
210
+
211
+ add_to_tree(tree, dirs)
212
+
213
+ return tree
214
+
215
+ def preview(self, console: Console, base_path: Path) -> None:
216
+ """Show formatted preview with tree and summary."""
217
+ if not self.files:
218
+ console.print("[yellow]No files to change[/yellow]")
219
+ return
220
+
221
+ # Show tree
222
+ console.print("\n[bold]File Structure:[/bold]")
223
+ console.print(self.build_tree(base_path))
224
+
225
+ # Summary
226
+ creates = sum(1 for f in self.files if f.change_type == 'create')
227
+ modifies = sum(1 for f in self.files if f.change_type == 'modify')
228
+ deletes = sum(1 for f in self.files if f.change_type == 'delete')
229
+ total_lines = sum(f.line_count for f in self.files)
230
+
231
+ summary_parts = []
232
+ if creates:
233
+ summary_parts.append(f"[green]{creates} created[/green]")
234
+ if modifies:
235
+ summary_parts.append(f"[yellow]{modifies} modified[/yellow]")
236
+ if deletes:
237
+ summary_parts.append(f"[red]{deletes} deleted[/red]")
238
+
239
+ summary = ", ".join(summary_parts)
240
+ console.print(f"\n[bold]Summary:[/bold] {summary} ({total_lines} total lines)\n")
241
+
242
+ def write_all(self, base_path: Path, dry_run: bool = False, console: Optional[Console] = None) -> None:
243
+ """Write all file changes to disk.
244
+
245
+ Args:
246
+ base_path: Base directory for file operations
247
+ dry_run: If True, don't actually write files
248
+ console: Rich console for output
249
+ """
250
+ if console is None:
251
+ console = Console()
252
+
253
+ base_path = base_path.resolve()
254
+ base_path.mkdir(parents=True, exist_ok=True)
255
+
256
+ for file_change in self.files:
257
+ full_path = base_path / file_change.path
258
+
259
+ if dry_run:
260
+ if file_change.change_type == 'create':
261
+ console.print(f"[dim]Would create: {file_change.path}[/dim]")
262
+ elif file_change.change_type == 'modify':
263
+ console.print(f"[dim]Would modify: {file_change.path}[/dim]")
264
+ elif file_change.change_type == 'delete':
265
+ console.print(f"[dim]Would delete: {file_change.path}[/dim]")
266
+ continue
267
+
268
+ # Actual file operations
269
+ if file_change.change_type == 'delete':
270
+ if full_path.exists():
271
+ full_path.unlink()
272
+ console.print(f"[red]✗[/red] Deleted: {file_change.path}")
273
+ else:
274
+ # Create parent directories
275
+ full_path.parent.mkdir(parents=True, exist_ok=True)
276
+
277
+ # Write file
278
+ full_path.write_text(file_change.content)
279
+
280
+ if file_change.change_type == 'create':
281
+ console.print(f"[green]✓[/green] Created: {file_change.path}")
282
+ elif file_change.change_type == 'modify':
283
+ console.print(f"[yellow]✓[/yellow] Modified: {file_change.path}")
284
+
285
+ def confirm(self, console: Console) -> bool:
286
+ """Interactive confirmation prompt.
287
+
288
+ Returns True if user confirms, False otherwise.
289
+ """
290
+ if not self.files:
291
+ return False
292
+
293
+ while True:
294
+ response = console.input("\n[cyan]Continue?[/cyan] [dim](Y/n/preview/help)[/dim] ").strip().lower()
295
+
296
+ if response in ('y', 'yes', ''):
297
+ return True
298
+ elif response in ('n', 'no'):
299
+ return False
300
+ elif response == 'preview':
301
+ # Show individual file contents
302
+ for i, file_change in enumerate(self.files, 1):
303
+ console.print(f"\n[bold]File {i}/{len(self.files)}:[/bold] {file_change.path}")
304
+
305
+ if file_change.change_type == 'delete':
306
+ console.print("[red]This file will be deleted[/red]")
307
+ elif file_change.change_type == 'modify' and file_change.diff:
308
+ console.print("[yellow]Diff:[/yellow]")
309
+ console.print(Panel(file_change.diff, border_style="yellow"))
310
+ else:
311
+ preview = file_change.content[:500]
312
+ if len(file_change.content) > 500:
313
+ preview += "\n... (truncated)"
314
+ console.print(Panel(preview, border_style="green"))
315
+ elif response == 'help':
316
+ console.print("""
317
+ [bold]Options:[/bold]
318
+ y, yes - Proceed with changes
319
+ n, no - Cancel
320
+ preview - Show file contents/diffs
321
+ help - Show this help
322
+ """)
323
+ else:
324
+ console.print("[red]Invalid response. Type 'help' for options.[/red]")
325
+
326
+
327
+ def extract_code_blocks(text: str) -> List[Tuple[str, str, str]]:
328
+ """Extract code blocks from markdown text.
329
+
330
+ Returns list of (file_marker, language, code) tuples.
331
+ """
332
+ pattern = r'^##\s+(File|Create|Modify|Delete):\s*(.+?)$.*?```(\w+)?\n(.*?)```'
333
+ matches = re.findall(pattern, text, re.MULTILINE | re.DOTALL)
334
+
335
+ results = []
336
+ for match in matches:
337
+ action = match[0]
338
+ path = match[1].strip()
339
+ language = match[2] if match[2] else ''
340
+ code = match[3].strip()
341
+ results.append((f"{action}: {path}", language, code))
342
+
343
+ return results
344
+
345
+
346
+ def count_lines(content: str) -> int:
347
+ """Count non-empty lines in content."""
348
+ return len([line for line in content.splitlines() if line.strip()])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.12.0
3
+ Version: 0.12.1
4
4
  Summary: A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking
5
5
  Author-email: Julio <thinmanj@users.noreply.github.com>
6
6
  License: MIT
@@ -10,6 +10,7 @@ src/claude_dev_cli/context.py
10
10
  src/claude_dev_cli/core.py
11
11
  src/claude_dev_cli/history.py
12
12
  src/claude_dev_cli/input_sources.py
13
+ src/claude_dev_cli/multi_file_handler.py
13
14
  src/claude_dev_cli/path_utils.py
14
15
  src/claude_dev_cli/secure_storage.py
15
16
  src/claude_dev_cli/template_manager.py
File without changes