claude-dev-cli 0.13.0__py3-none-any.whl → 0.16.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.
Potentially problematic release.
This version of claude-dev-cli might be problematic. Click here for more details.
- claude_dev_cli/__init__.py +1 -1
- claude_dev_cli/cli.py +234 -16
- claude_dev_cli/config.py +95 -9
- claude_dev_cli/core.py +48 -53
- claude_dev_cli/multi_file_handler.py +400 -16
- claude_dev_cli/providers/__init__.py +28 -0
- claude_dev_cli/providers/anthropic.py +216 -0
- claude_dev_cli/providers/base.py +168 -0
- claude_dev_cli/providers/factory.py +114 -0
- claude_dev_cli/providers/ollama.py +283 -0
- claude_dev_cli/providers/openai.py +268 -0
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.16.0.dist-info}/METADATA +297 -15
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.16.0.dist-info}/RECORD +17 -11
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.16.0.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.16.0.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.16.0.dist-info}/top_level.txt +0 -0
|
@@ -3,11 +3,50 @@
|
|
|
3
3
|
import re
|
|
4
4
|
import difflib
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import List, Tuple, Optional, Literal
|
|
7
|
-
from dataclasses import dataclass
|
|
6
|
+
from typing import List, Tuple, Optional, Literal, Dict, Any
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.tree import Tree
|
|
10
10
|
from rich.panel import Panel
|
|
11
|
+
from rich.syntax import Syntax
|
|
12
|
+
from io import StringIO
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from unidiff import PatchSet
|
|
16
|
+
UNIDIFF_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
UNIDIFF_AVAILABLE = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class HunkWrapper:
|
|
23
|
+
"""Wrapper around unidiff.Hunk with approval state."""
|
|
24
|
+
hunk: Any # unidiff.Hunk
|
|
25
|
+
approved: bool = False
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def source_start(self) -> int:
|
|
29
|
+
"""Get source start line number."""
|
|
30
|
+
return self.hunk.source_start if hasattr(self.hunk, 'source_start') else 0
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def source_length(self) -> int:
|
|
34
|
+
"""Get source length."""
|
|
35
|
+
return self.hunk.source_length if hasattr(self.hunk, 'source_length') else 0
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def target_start(self) -> int:
|
|
39
|
+
"""Get target start line number."""
|
|
40
|
+
return self.hunk.target_start if hasattr(self.hunk, 'target_start') else 0
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def target_length(self) -> int:
|
|
44
|
+
"""Get target length."""
|
|
45
|
+
return self.hunk.target_length if hasattr(self.hunk, 'target_length') else 0
|
|
46
|
+
|
|
47
|
+
def __str__(self) -> str:
|
|
48
|
+
"""Format hunk as unified diff text."""
|
|
49
|
+
return str(self.hunk)
|
|
11
50
|
|
|
12
51
|
|
|
13
52
|
@dataclass
|
|
@@ -17,6 +56,7 @@ class FileChange:
|
|
|
17
56
|
content: str
|
|
18
57
|
change_type: Literal["create", "modify", "delete"]
|
|
19
58
|
original_content: Optional[str] = None
|
|
59
|
+
hunks: List[HunkWrapper] = field(default_factory=list)
|
|
20
60
|
|
|
21
61
|
@property
|
|
22
62
|
def line_count(self) -> int:
|
|
@@ -36,11 +76,83 @@ class FileChange:
|
|
|
36
76
|
original_lines,
|
|
37
77
|
new_lines,
|
|
38
78
|
fromfile=f"a/{self.path}",
|
|
39
|
-
tofile=f"b/{self.path}"
|
|
40
|
-
lineterm=''
|
|
79
|
+
tofile=f"b/{self.path}"
|
|
41
80
|
))
|
|
42
81
|
|
|
43
82
|
return ''.join(diff_lines) if diff_lines else None
|
|
83
|
+
|
|
84
|
+
def parse_hunks(self) -> None:
|
|
85
|
+
"""Parse diff into individual hunks using unidiff library."""
|
|
86
|
+
if self.change_type != "modify" or not self.diff:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if not UNIDIFF_AVAILABLE:
|
|
90
|
+
# Fallback: no hunk parsing available
|
|
91
|
+
self.hunks = []
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
self.hunks = []
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Parse the diff with unidiff
|
|
98
|
+
patch = PatchSet(StringIO(self.diff))
|
|
99
|
+
|
|
100
|
+
# Extract all hunks from all patched files
|
|
101
|
+
for patched_file in patch:
|
|
102
|
+
for hunk in patched_file:
|
|
103
|
+
self.hunks.append(HunkWrapper(hunk=hunk, approved=False))
|
|
104
|
+
|
|
105
|
+
except Exception:
|
|
106
|
+
# If unidiff fails, fall back to empty hunks list
|
|
107
|
+
self.hunks = []
|
|
108
|
+
|
|
109
|
+
def apply_approved_hunks(self) -> str:
|
|
110
|
+
"""Apply only approved hunks using unidiff's line access methods."""
|
|
111
|
+
if self.change_type != "modify" or not self.original_content:
|
|
112
|
+
return self.content
|
|
113
|
+
|
|
114
|
+
# If no hunks parsed, caller decides what to do
|
|
115
|
+
# In write_all, we check if hunks exist before calling this
|
|
116
|
+
if not self.hunks:
|
|
117
|
+
return self.content
|
|
118
|
+
|
|
119
|
+
# If no hunks approved, return original
|
|
120
|
+
if not any(h.approved for h in self.hunks):
|
|
121
|
+
return self.original_content
|
|
122
|
+
|
|
123
|
+
# If all hunks approved, return new content
|
|
124
|
+
if all(h.approved for h in self.hunks):
|
|
125
|
+
return self.content
|
|
126
|
+
|
|
127
|
+
# Apply only approved hunks
|
|
128
|
+
original_lines = self.original_content.splitlines(keepends=True)
|
|
129
|
+
result_lines = original_lines.copy()
|
|
130
|
+
|
|
131
|
+
# Sort hunks by position (reversed for bottom-up application)
|
|
132
|
+
sorted_hunks = sorted(
|
|
133
|
+
[h for h in self.hunks if h.approved],
|
|
134
|
+
key=lambda h: h.source_start,
|
|
135
|
+
reverse=True
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
for wrapper in sorted_hunks:
|
|
139
|
+
hunk = wrapper.hunk
|
|
140
|
+
|
|
141
|
+
# Calculate indices for replacement
|
|
142
|
+
start_idx = wrapper.source_start - 1
|
|
143
|
+
end_idx = start_idx + wrapper.source_length
|
|
144
|
+
|
|
145
|
+
# Extract new lines using unidiff's line iteration
|
|
146
|
+
new_lines = []
|
|
147
|
+
for line in hunk:
|
|
148
|
+
if line.is_added or line.is_context:
|
|
149
|
+
# Get line value (already includes newline)
|
|
150
|
+
new_lines.append(line.value)
|
|
151
|
+
|
|
152
|
+
# Replace section
|
|
153
|
+
result_lines[start_idx:end_idx] = new_lines
|
|
154
|
+
|
|
155
|
+
return ''.join(result_lines)
|
|
44
156
|
|
|
45
157
|
|
|
46
158
|
class MultiFileResponse:
|
|
@@ -254,13 +366,25 @@ class MultiFileResponse:
|
|
|
254
366
|
base_path.mkdir(parents=True, exist_ok=True)
|
|
255
367
|
|
|
256
368
|
for file_change in self.files:
|
|
369
|
+
# Skip files marked for skipping
|
|
370
|
+
if hasattr(file_change, 'change_type') and file_change.change_type == 'skip':
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
# Skip empty content for create (marked as rejected)
|
|
374
|
+
if file_change.change_type == 'create' and not file_change.content:
|
|
375
|
+
continue
|
|
376
|
+
|
|
257
377
|
full_path = base_path / file_change.path
|
|
258
378
|
|
|
259
379
|
if dry_run:
|
|
260
380
|
if file_change.change_type == 'create':
|
|
261
381
|
console.print(f"[dim]Would create: {file_change.path}[/dim]")
|
|
262
382
|
elif file_change.change_type == 'modify':
|
|
263
|
-
|
|
383
|
+
if file_change.hunks and any(h.approved for h in file_change.hunks):
|
|
384
|
+
approved_count = sum(1 for h in file_change.hunks if h.approved)
|
|
385
|
+
console.print(f"[dim]Would modify: {file_change.path} ({approved_count}/{len(file_change.hunks)} hunks)[/dim]")
|
|
386
|
+
else:
|
|
387
|
+
console.print(f"[dim]Would modify: {file_change.path}[/dim]")
|
|
264
388
|
elif file_change.change_type == 'delete':
|
|
265
389
|
console.print(f"[dim]Would delete: {file_change.path}[/dim]")
|
|
266
390
|
continue
|
|
@@ -274,29 +398,60 @@ class MultiFileResponse:
|
|
|
274
398
|
# Create parent directories
|
|
275
399
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
276
400
|
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
401
|
+
# For modify with hunks, apply only approved hunks
|
|
402
|
+
if file_change.change_type == 'modify' and file_change.hunks:
|
|
403
|
+
# Only write if at least one hunk is approved
|
|
404
|
+
if not any(h.approved for h in file_change.hunks):
|
|
405
|
+
console.print(f"[dim]Skipped: {file_change.path} (no hunks approved)[/dim]")
|
|
406
|
+
continue
|
|
407
|
+
|
|
408
|
+
content_to_write = file_change.apply_approved_hunks()
|
|
409
|
+
full_path.write_text(content_to_write)
|
|
410
|
+
approved_count = sum(1 for h in file_change.hunks if h.approved)
|
|
411
|
+
console.print(f"[yellow]✓[/yellow] Modified: {file_change.path} ({approved_count}/{len(file_change.hunks)} hunks)")
|
|
412
|
+
else:
|
|
413
|
+
# Write file normally (no hunks or create operation)
|
|
414
|
+
full_path.write_text(file_change.content)
|
|
415
|
+
|
|
416
|
+
if file_change.change_type == 'create':
|
|
417
|
+
console.print(f"[green]✓[/green] Created: {file_change.path}")
|
|
418
|
+
elif file_change.change_type == 'modify':
|
|
419
|
+
console.print(f"[yellow]✓[/yellow] Modified: {file_change.path}")
|
|
284
420
|
|
|
285
|
-
def confirm(self, console: Console) -> bool:
|
|
421
|
+
def confirm(self, console: Console, base_path: Optional[Path] = None) -> bool:
|
|
286
422
|
"""Interactive confirmation prompt.
|
|
287
423
|
|
|
424
|
+
Args:
|
|
425
|
+
console: Rich console for output
|
|
426
|
+
base_path: Base directory for file operations (needed for edit/save)
|
|
427
|
+
|
|
288
428
|
Returns True if user confirms, False otherwise.
|
|
289
429
|
"""
|
|
290
430
|
if not self.files:
|
|
291
431
|
return False
|
|
292
432
|
|
|
293
433
|
while True:
|
|
294
|
-
response = console.input("\n[cyan]Continue?[/cyan] [dim](Y/n/preview/help)[/dim] ").strip().lower()
|
|
434
|
+
response = console.input("\n[cyan]Continue?[/cyan] [dim](Y/n/preview/patch/edit/save/help)[/dim] ").strip().lower()
|
|
295
435
|
|
|
296
436
|
if response in ('y', 'yes', ''):
|
|
297
437
|
return True
|
|
298
438
|
elif response in ('n', 'no'):
|
|
299
439
|
return False
|
|
440
|
+
elif response == 'patch':
|
|
441
|
+
# Use hunk-by-hunk confirmation
|
|
442
|
+
return self.confirm_with_hunks(console)
|
|
443
|
+
elif response == 'edit':
|
|
444
|
+
# Open files in editor for manual editing
|
|
445
|
+
if base_path:
|
|
446
|
+
self._edit_files(console, base_path)
|
|
447
|
+
else:
|
|
448
|
+
console.print("[red]Edit not available (no base path provided)[/red]")
|
|
449
|
+
elif response == 'save':
|
|
450
|
+
# Save to custom location
|
|
451
|
+
if base_path:
|
|
452
|
+
return self._save_to_location(console, base_path)
|
|
453
|
+
else:
|
|
454
|
+
console.print("[red]Save not available (no base path provided)[/red]")
|
|
300
455
|
elif response == 'preview':
|
|
301
456
|
# Show individual file contents
|
|
302
457
|
for i, file_change in enumerate(self.files, 1):
|
|
@@ -315,13 +470,242 @@ class MultiFileResponse:
|
|
|
315
470
|
elif response == 'help':
|
|
316
471
|
console.print("""
|
|
317
472
|
[bold]Options:[/bold]
|
|
318
|
-
y, yes - Proceed with changes
|
|
319
|
-
n, no - Cancel
|
|
473
|
+
y, yes - Proceed with all changes
|
|
474
|
+
n, no - Cancel all changes
|
|
475
|
+
patch - Review changes hunk-by-hunk (like git add -p)
|
|
476
|
+
edit - Open files in $EDITOR before applying
|
|
477
|
+
save - Save to custom location/filename
|
|
320
478
|
preview - Show file contents/diffs
|
|
321
479
|
help - Show this help
|
|
322
480
|
""")
|
|
323
481
|
else:
|
|
324
482
|
console.print("[red]Invalid response. Type 'help' for options.[/red]")
|
|
483
|
+
|
|
484
|
+
def confirm_with_hunks(self, console: Console) -> bool:
|
|
485
|
+
"""Interactive hunk-by-hunk confirmation (like git add -p).
|
|
486
|
+
|
|
487
|
+
Returns True if at least some changes approved, False if all cancelled.
|
|
488
|
+
"""
|
|
489
|
+
if not self.files:
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
# Parse hunks for all modify operations
|
|
493
|
+
for file_change in self.files:
|
|
494
|
+
if file_change.change_type == 'modify':
|
|
495
|
+
file_change.parse_hunks()
|
|
496
|
+
|
|
497
|
+
has_any_approval = False
|
|
498
|
+
|
|
499
|
+
for file_change in self.files:
|
|
500
|
+
console.print(f"\n[bold cyan]File:[/bold cyan] {file_change.path}")
|
|
501
|
+
|
|
502
|
+
if file_change.change_type == 'create':
|
|
503
|
+
console.print(f"[green]Create new file ({file_change.line_count} lines)[/green]")
|
|
504
|
+
response = self._ask_file_action(console, "create")
|
|
505
|
+
if response == 'y':
|
|
506
|
+
# Mark as approved (keep as-is)
|
|
507
|
+
has_any_approval = True
|
|
508
|
+
elif response == 'n':
|
|
509
|
+
# Remove from files list
|
|
510
|
+
file_change.content = '' # Mark for skip
|
|
511
|
+
elif response == 'q':
|
|
512
|
+
return has_any_approval
|
|
513
|
+
|
|
514
|
+
elif file_change.change_type == 'delete':
|
|
515
|
+
console.print("[red]Delete file[/red]")
|
|
516
|
+
response = self._ask_file_action(console, "delete")
|
|
517
|
+
if response == 'y':
|
|
518
|
+
has_any_approval = True
|
|
519
|
+
elif response == 'n':
|
|
520
|
+
file_change.change_type = 'skip' # Mark for skip
|
|
521
|
+
elif response == 'q':
|
|
522
|
+
return has_any_approval
|
|
523
|
+
|
|
524
|
+
elif file_change.change_type == 'modify':
|
|
525
|
+
if not file_change.hunks:
|
|
526
|
+
console.print("[yellow]No hunks to review[/yellow]")
|
|
527
|
+
continue
|
|
528
|
+
|
|
529
|
+
console.print(f"[yellow]Modify file ({len(file_change.hunks)} hunk(s))[/yellow]")
|
|
530
|
+
|
|
531
|
+
for hunk_idx, hunk in enumerate(file_change.hunks, 1):
|
|
532
|
+
console.print(f"\n[bold]Hunk {hunk_idx}/{len(file_change.hunks)}:[/bold]")
|
|
533
|
+
|
|
534
|
+
# Show hunk with syntax highlighting
|
|
535
|
+
hunk_text = str(hunk)
|
|
536
|
+
console.print(Panel(
|
|
537
|
+
Syntax(hunk_text, "diff", theme="monokai", line_numbers=False),
|
|
538
|
+
border_style="yellow",
|
|
539
|
+
title=f"[bold]{file_change.path}[/bold]"
|
|
540
|
+
))
|
|
541
|
+
|
|
542
|
+
while True:
|
|
543
|
+
response = console.input(
|
|
544
|
+
"[cyan]Apply this hunk?[/cyan] [dim](y/n/s=skip file/q=quit/help)[/dim] "
|
|
545
|
+
).strip().lower()
|
|
546
|
+
|
|
547
|
+
if response in ('y', 'yes', ''):
|
|
548
|
+
hunk.approved = True
|
|
549
|
+
has_any_approval = True
|
|
550
|
+
break
|
|
551
|
+
elif response in ('n', 'no'):
|
|
552
|
+
hunk.approved = False
|
|
553
|
+
break
|
|
554
|
+
elif response in ('s', 'skip'):
|
|
555
|
+
# Skip remaining hunks in this file
|
|
556
|
+
break
|
|
557
|
+
elif response in ('q', 'quit'):
|
|
558
|
+
return has_any_approval
|
|
559
|
+
elif response == 'help':
|
|
560
|
+
console.print("""
|
|
561
|
+
[bold]Hunk Options:[/bold]
|
|
562
|
+
y, yes - Apply this hunk
|
|
563
|
+
n, no - Skip this hunk
|
|
564
|
+
s, skip - Skip remaining hunks in this file
|
|
565
|
+
q, quit - Quit and apply approved hunks so far
|
|
566
|
+
help - Show this help
|
|
567
|
+
""")
|
|
568
|
+
else:
|
|
569
|
+
console.print("[red]Invalid response. Type 'help' for options.[/red]")
|
|
570
|
+
|
|
571
|
+
if response in ('s', 'skip'):
|
|
572
|
+
break
|
|
573
|
+
|
|
574
|
+
return has_any_approval
|
|
575
|
+
|
|
576
|
+
def _ask_file_action(self, console: Console, action: str) -> str:
|
|
577
|
+
"""Ask for confirmation on file-level action.
|
|
578
|
+
|
|
579
|
+
Returns: 'y' (yes), 'n' (no), 's' (skip), 'q' (quit)
|
|
580
|
+
"""
|
|
581
|
+
while True:
|
|
582
|
+
response = console.input(
|
|
583
|
+
f"[cyan]{action.capitalize()} this file?[/cyan] [dim](y/n/s=skip/q=quit)[/dim] "
|
|
584
|
+
).strip().lower()
|
|
585
|
+
|
|
586
|
+
if response in ('y', 'yes', 'n', 'no', 's', 'skip', 'q', 'quit', ''):
|
|
587
|
+
if response == '':
|
|
588
|
+
return 'y'
|
|
589
|
+
if response in ('skip',):
|
|
590
|
+
return 's'
|
|
591
|
+
if response in ('quit',):
|
|
592
|
+
return 'q'
|
|
593
|
+
return response[0] # Return first character
|
|
594
|
+
else:
|
|
595
|
+
console.print("[red]Invalid response. Use y/n/s/q[/red]")
|
|
596
|
+
|
|
597
|
+
def _edit_files(self, console: Console, base_path: Path) -> None:
|
|
598
|
+
"""Open files in editor for manual editing before applying.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
console: Rich console for output
|
|
602
|
+
base_path: Base directory for file operations
|
|
603
|
+
"""
|
|
604
|
+
import os
|
|
605
|
+
import subprocess
|
|
606
|
+
import tempfile
|
|
607
|
+
|
|
608
|
+
editor = os.environ.get('EDITOR', 'vi')
|
|
609
|
+
|
|
610
|
+
console.print(f"\n[cyan]Opening files in {editor}...[/cyan]")
|
|
611
|
+
console.print("[dim]Save and close editor to continue[/dim]\n")
|
|
612
|
+
|
|
613
|
+
for file_change in self.files:
|
|
614
|
+
if file_change.change_type == 'delete':
|
|
615
|
+
console.print(f"[dim]Skipping delete operation: {file_change.path}[/dim]")
|
|
616
|
+
continue
|
|
617
|
+
|
|
618
|
+
# Write content to temp file
|
|
619
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix=f'_{Path(file_change.path).name}', delete=False) as tf:
|
|
620
|
+
tf.write(file_change.content)
|
|
621
|
+
temp_path = tf.name
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
console.print(f"[bold]Editing:[/bold] {file_change.path}")
|
|
625
|
+
|
|
626
|
+
# Open in editor
|
|
627
|
+
result = subprocess.run([editor, temp_path])
|
|
628
|
+
|
|
629
|
+
if result.returncode == 0:
|
|
630
|
+
# Read edited content
|
|
631
|
+
with open(temp_path, 'r') as f:
|
|
632
|
+
edited_content = f.read()
|
|
633
|
+
|
|
634
|
+
# Update file change with edited content
|
|
635
|
+
file_change.content = edited_content
|
|
636
|
+
console.print(f"[green]✓[/green] Updated: {file_change.path}")
|
|
637
|
+
else:
|
|
638
|
+
console.print(f"[yellow]Editor exited with error, keeping original content[/yellow]")
|
|
639
|
+
finally:
|
|
640
|
+
# Clean up temp file
|
|
641
|
+
try:
|
|
642
|
+
os.unlink(temp_path)
|
|
643
|
+
except:
|
|
644
|
+
pass
|
|
645
|
+
|
|
646
|
+
console.print("\n[green]Editing complete![/green]")
|
|
647
|
+
|
|
648
|
+
def _save_to_location(self, console: Console, base_path: Path) -> bool:
|
|
649
|
+
"""Save files to custom location.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
console: Rich console for output
|
|
653
|
+
base_path: Base directory for file operations
|
|
654
|
+
|
|
655
|
+
Returns: True if saved successfully, False otherwise
|
|
656
|
+
"""
|
|
657
|
+
console.print("\n[cyan]Save options:[/cyan]")
|
|
658
|
+
|
|
659
|
+
if len(self.files) == 1:
|
|
660
|
+
# Single file - ask for filename
|
|
661
|
+
console.print("[dim]Enter filename (or path) to save to:[/dim]")
|
|
662
|
+
filename = console.input("[cyan]Filename:[/cyan] ").strip()
|
|
663
|
+
|
|
664
|
+
if not filename:
|
|
665
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
666
|
+
return False
|
|
667
|
+
|
|
668
|
+
save_path = Path(filename)
|
|
669
|
+
if not save_path.is_absolute():
|
|
670
|
+
save_path = base_path / save_path
|
|
671
|
+
|
|
672
|
+
# Create parent directories
|
|
673
|
+
save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
674
|
+
|
|
675
|
+
# Write file
|
|
676
|
+
file_change = self.files[0]
|
|
677
|
+
save_path.write_text(file_change.content)
|
|
678
|
+
console.print(f"[green]✓[/green] Saved to: {save_path}")
|
|
679
|
+
return False # Don't continue with original write
|
|
680
|
+
else:
|
|
681
|
+
# Multiple files - ask for directory
|
|
682
|
+
console.print(f"[dim]Enter directory to save {len(self.files)} file(s) to:[/dim]")
|
|
683
|
+
dirname = console.input("[cyan]Directory:[/cyan] ").strip()
|
|
684
|
+
|
|
685
|
+
if not dirname:
|
|
686
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
687
|
+
return False
|
|
688
|
+
|
|
689
|
+
save_dir = Path(dirname)
|
|
690
|
+
if not save_dir.is_absolute():
|
|
691
|
+
save_dir = base_path / save_dir
|
|
692
|
+
|
|
693
|
+
# Create directory
|
|
694
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
695
|
+
|
|
696
|
+
# Write all files
|
|
697
|
+
for file_change in self.files:
|
|
698
|
+
if file_change.change_type == 'delete':
|
|
699
|
+
console.print(f"[dim]Skipping delete operation: {file_change.path}[/dim]")
|
|
700
|
+
continue
|
|
701
|
+
|
|
702
|
+
file_path = save_dir / file_change.path
|
|
703
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
704
|
+
file_path.write_text(file_change.content)
|
|
705
|
+
console.print(f"[green]✓[/green] Saved: {file_path}")
|
|
706
|
+
|
|
707
|
+
console.print(f"\n[green]All files saved to: {save_dir}[/green]")
|
|
708
|
+
return False # Don't continue with original write
|
|
325
709
|
|
|
326
710
|
|
|
327
711
|
def extract_code_blocks(text: str) -> List[Tuple[str, str, str]]:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""AI Provider abstraction layer for claude-dev-cli.
|
|
2
|
+
|
|
3
|
+
This package provides a unified interface for multiple AI providers:
|
|
4
|
+
- Anthropic (Claude)
|
|
5
|
+
- OpenAI (GPT-4, GPT-3.5) - coming in v0.15.0
|
|
6
|
+
- Ollama (Local models) - coming in v0.16.0
|
|
7
|
+
- LM Studio (Local models) - coming in v0.16.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from claude_dev_cli.providers.base import (
|
|
11
|
+
AIProvider,
|
|
12
|
+
ModelInfo,
|
|
13
|
+
UsageInfo,
|
|
14
|
+
ProviderError,
|
|
15
|
+
InsufficientCreditsError,
|
|
16
|
+
ProviderConnectionError,
|
|
17
|
+
ModelNotFoundError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AIProvider",
|
|
22
|
+
"ModelInfo",
|
|
23
|
+
"UsageInfo",
|
|
24
|
+
"ProviderError",
|
|
25
|
+
"InsufficientCreditsError",
|
|
26
|
+
"ProviderConnectionError",
|
|
27
|
+
"ModelNotFoundError",
|
|
28
|
+
]
|