claude-dev-cli 0.1.0__py3-none-any.whl → 0.3.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.
@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.1.0"
12
+ __version__ = "0.2.0"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
claude_dev_cli/cli.py CHANGED
@@ -21,6 +21,8 @@ from claude_dev_cli.commands import (
21
21
  git_commit_message,
22
22
  )
23
23
  from claude_dev_cli.usage import UsageTracker
24
+ from claude_dev_cli import toon_utils
25
+ from claude_dev_cli.plugins import load_plugins
24
26
 
25
27
  console = Console()
26
28
 
@@ -34,6 +36,17 @@ def main(ctx: click.Context) -> None:
34
36
  ctx.obj['console'] = console
35
37
 
36
38
 
39
+ # Load plugins after main group is defined
40
+ # Silently load plugins - they'll register their commands
41
+ try:
42
+ plugins = load_plugins()
43
+ for plugin in plugins:
44
+ plugin.register_commands(main)
45
+ except Exception:
46
+ # Don't fail if plugins can't load - continue without them
47
+ pass
48
+
49
+
37
50
  @main.command()
38
51
  @click.argument('prompt', required=False)
39
52
  @click.option('-f', '--file', type=click.Path(exists=True), help='Include file content in prompt')
@@ -394,5 +407,124 @@ def usage(ctx: click.Context, days: Optional[int], api: Optional[str]) -> None:
394
407
  sys.exit(1)
395
408
 
396
409
 
410
+ @main.group()
411
+ def toon() -> None:
412
+ """TOON format conversion tools."""
413
+ pass
414
+
415
+
416
+ @toon.command('encode')
417
+ @click.argument('input_file', type=click.Path(exists=True), required=False)
418
+ @click.option('-o', '--output', type=click.Path(), help='Output file')
419
+ @click.pass_context
420
+ def toon_encode(ctx: click.Context, input_file: Optional[str], output: Optional[str]) -> None:
421
+ """Convert JSON to TOON format."""
422
+ console = ctx.obj['console']
423
+
424
+ if not toon_utils.is_toon_available():
425
+ console.print("[red]TOON support not installed.[/red]")
426
+ console.print("Install with: [cyan]pip install claude-dev-cli[toon][/cyan]")
427
+ sys.exit(1)
428
+
429
+ try:
430
+ import json
431
+
432
+ # Read input
433
+ if input_file:
434
+ with open(input_file, 'r') as f:
435
+ data = json.load(f)
436
+ elif not sys.stdin.isatty():
437
+ data = json.load(sys.stdin)
438
+ else:
439
+ console.print("[red]Error: No input provided[/red]")
440
+ console.print("Usage: cdc toon encode [FILE] or pipe JSON via stdin")
441
+ sys.exit(1)
442
+
443
+ # Convert to TOON
444
+ toon_str = toon_utils.to_toon(data)
445
+
446
+ # Output
447
+ if output:
448
+ with open(output, 'w') as f:
449
+ f.write(toon_str)
450
+ console.print(f"[green]✓[/green] Converted to TOON: {output}")
451
+ else:
452
+ console.print(toon_str)
453
+
454
+ except Exception as e:
455
+ console.print(f"[red]Error: {e}[/red]")
456
+ sys.exit(1)
457
+
458
+
459
+ @toon.command('decode')
460
+ @click.argument('input_file', type=click.Path(exists=True), required=False)
461
+ @click.option('-o', '--output', type=click.Path(), help='Output file')
462
+ @click.pass_context
463
+ def toon_decode(ctx: click.Context, input_file: Optional[str], output: Optional[str]) -> None:
464
+ """Convert TOON format to JSON."""
465
+ console = ctx.obj['console']
466
+
467
+ if not toon_utils.is_toon_available():
468
+ console.print("[red]TOON support not installed.[/red]")
469
+ console.print("Install with: [cyan]pip install claude-dev-cli[toon][/cyan]")
470
+ sys.exit(1)
471
+
472
+ try:
473
+ import json
474
+
475
+ # Read input
476
+ if input_file:
477
+ with open(input_file, 'r') as f:
478
+ toon_str = f.read()
479
+ elif not sys.stdin.isatty():
480
+ toon_str = sys.stdin.read()
481
+ else:
482
+ console.print("[red]Error: No input provided[/red]")
483
+ console.print("Usage: cdc toon decode [FILE] or pipe TOON via stdin")
484
+ sys.exit(1)
485
+
486
+ # Convert from TOON
487
+ data = toon_utils.from_toon(toon_str)
488
+
489
+ # Output
490
+ json_str = json.dumps(data, indent=2)
491
+ if output:
492
+ with open(output, 'w') as f:
493
+ f.write(json_str)
494
+ console.print(f"[green]✓[/green] Converted to JSON: {output}")
495
+ else:
496
+ console.print(json_str)
497
+
498
+ except Exception as e:
499
+ console.print(f"[red]Error: {e}[/red]")
500
+ sys.exit(1)
501
+
502
+
503
+ @toon.command('info')
504
+ @click.pass_context
505
+ def toon_info(ctx: click.Context) -> None:
506
+ """Show TOON format installation status and token savings info."""
507
+ console = ctx.obj['console']
508
+
509
+ if toon_utils.is_toon_available():
510
+ console.print("[green]✓[/green] TOON format support is installed")
511
+ console.print("\n[bold]About TOON:[/bold]")
512
+ console.print("• Token-Oriented Object Notation")
513
+ console.print("• 30-60% fewer tokens than JSON")
514
+ console.print("• Optimized for LLM prompts")
515
+ console.print("• Human-readable and lossless")
516
+ console.print("\n[bold]Usage:[/bold]")
517
+ console.print(" cdc toon encode data.json -o data.toon")
518
+ console.print(" cdc toon decode data.toon -o data.json")
519
+ console.print(" cat data.json | cdc toon encode")
520
+ else:
521
+ console.print("[yellow]TOON format support not installed[/yellow]")
522
+ console.print("\nInstall with: [cyan]pip install claude-dev-cli[toon][/cyan]")
523
+ console.print("\n[bold]Benefits:[/bold]")
524
+ console.print("• Reduce API costs by 30-60%")
525
+ console.print("• Faster LLM response times")
526
+ console.print("• Same data, fewer tokens")
527
+
528
+
397
529
  if __name__ == '__main__':
398
530
  main(obj={})
@@ -0,0 +1,42 @@
1
+ """Plugin system for Claude Dev CLI."""
2
+
3
+ from typing import List
4
+ from pathlib import Path
5
+ import importlib
6
+ import importlib.util
7
+
8
+ from .base import Plugin
9
+
10
+
11
+ def discover_plugins() -> List[Plugin]:
12
+ """Discover and load all plugins from the plugins directory.
13
+
14
+ Returns:
15
+ List of loaded plugin instances
16
+ """
17
+ plugins = []
18
+ plugin_dir = Path(__file__).parent
19
+
20
+ # Look for plugin directories (those with plugin.py)
21
+ for item in plugin_dir.iterdir():
22
+ if item.is_dir() and (item / "plugin.py").exists() and item.name != "__pycache__":
23
+ try:
24
+ # Use proper import instead of spec loading to handle relative imports
25
+ plugin_module_name = f"claude_dev_cli.plugins.{item.name}.plugin"
26
+ module = importlib.import_module(plugin_module_name)
27
+
28
+ # Look for plugin registration function
29
+ if hasattr(module, "register_plugin"):
30
+ plugin = module.register_plugin()
31
+ plugins.append(plugin)
32
+ except Exception as e:
33
+ # Silently skip plugins that fail to load
34
+ pass
35
+
36
+ return plugins
37
+
38
+
39
+ # Alias for backwards compatibility and clearer intent
40
+ load_plugins = discover_plugins
41
+
42
+ __all__ = ["Plugin", "discover_plugins", "load_plugins"]
@@ -0,0 +1,53 @@
1
+ """Base plugin interface for Claude Dev CLI."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+ import click
6
+
7
+
8
+ class Plugin(ABC):
9
+ """Base class for all plugins."""
10
+
11
+ def __init__(self, name: str, version: str, description: str = ""):
12
+ """Initialize plugin.
13
+
14
+ Args:
15
+ name: Plugin name
16
+ version: Plugin version
17
+ description: Plugin description
18
+ """
19
+ self.name = name
20
+ self.version = version
21
+ self.description = description
22
+
23
+ @abstractmethod
24
+ def register_commands(self, cli: click.Group) -> None:
25
+ """Register plugin commands with the CLI.
26
+
27
+ Args:
28
+ cli: Click group to register commands to
29
+ """
30
+ pass
31
+
32
+ def before_apply(self, original: str, proposed: str) -> Optional[str]:
33
+ """Hook called before applying changes.
34
+
35
+ Args:
36
+ original: Original content
37
+ proposed: Proposed changes
38
+
39
+ Returns:
40
+ Modified proposed content or None to keep original
41
+ """
42
+ return None
43
+
44
+ def after_apply(self, result: str) -> Optional[str]:
45
+ """Hook called after applying changes.
46
+
47
+ Args:
48
+ result: Result after changes applied
49
+
50
+ Returns:
51
+ Modified result or None to keep original
52
+ """
53
+ return None
@@ -0,0 +1,3 @@
1
+ """Interactive diff editor plugin for reviewing AI-generated code changes."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,98 @@
1
+ """Diff editor plugin registration."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.syntax import Syntax
10
+
11
+ from claude_dev_cli.plugins.base import Plugin
12
+ from .viewer import DiffViewer
13
+
14
+
15
+ class DiffEditorPlugin(Plugin):
16
+ """Interactive diff editor plugin."""
17
+
18
+ def __init__(self):
19
+ super().__init__(
20
+ name="diff-editor",
21
+ version="0.1.0",
22
+ description="Interactive diff viewer for reviewing code changes"
23
+ )
24
+ self.console = Console()
25
+
26
+ def register_commands(self, cli: click.Group) -> None:
27
+ """Register diff editor commands."""
28
+
29
+ @cli.command("diff")
30
+ @click.argument("original", type=click.Path(exists=True))
31
+ @click.argument("proposed", type=click.Path(exists=True))
32
+ @click.option(
33
+ "--keybindings",
34
+ "-k",
35
+ type=click.Choice(["nvim", "fresh", "auto"]),
36
+ default="auto",
37
+ help="Keybinding mode (nvim, fresh, or auto-detect)"
38
+ )
39
+ @click.option(
40
+ "--output",
41
+ "-o",
42
+ type=click.Path(),
43
+ help="Output file path for accepted changes"
44
+ )
45
+ def diff_command(
46
+ original: str,
47
+ proposed: str,
48
+ keybindings: str,
49
+ output: Optional[str]
50
+ ) -> None:
51
+ """Interactively review differences between two files."""
52
+ viewer = DiffViewer(
53
+ original_path=Path(original),
54
+ proposed_path=Path(proposed),
55
+ keybinding_mode=keybindings,
56
+ console=self.console
57
+ )
58
+
59
+ result = viewer.run()
60
+
61
+ if result and output:
62
+ with open(output, "w") as f:
63
+ f.write(result)
64
+ self.console.print(f"\n[green]✓[/green] Changes saved to: {output}")
65
+ elif result:
66
+ self.console.print("\n[bold]Final result:[/bold]")
67
+ self.console.print(result)
68
+
69
+ @cli.command("apply-diff")
70
+ @click.argument("file_path", type=click.Path(exists=True))
71
+ @click.option(
72
+ "--keybindings",
73
+ "-k",
74
+ type=click.Choice(["nvim", "fresh", "auto"]),
75
+ default="auto",
76
+ help="Keybinding mode"
77
+ )
78
+ @click.option(
79
+ "--in-place",
80
+ "-i",
81
+ is_flag=True,
82
+ help="Edit file in place"
83
+ )
84
+ def apply_diff_command(
85
+ file_path: str,
86
+ keybindings: str,
87
+ in_place: bool
88
+ ) -> None:
89
+ """Apply AI-suggested changes to a file interactively."""
90
+ self.console.print(
91
+ f"[yellow]This would apply changes to {file_path} interactively[/yellow]"
92
+ )
93
+ self.console.print("Feature coming soon!")
94
+
95
+
96
+ def register_plugin() -> Plugin:
97
+ """Register the diff editor plugin."""
98
+ return DiffEditorPlugin()
@@ -0,0 +1,314 @@
1
+ """Interactive diff viewer with multiple keybinding modes."""
2
+
3
+ import difflib
4
+ from pathlib import Path
5
+ from typing import List, Optional, Tuple
6
+ import os
7
+
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.syntax import Syntax
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+
15
+ class Hunk:
16
+ """Represents a single diff hunk."""
17
+
18
+ def __init__(
19
+ self,
20
+ original_lines: List[str],
21
+ proposed_lines: List[str],
22
+ original_start: int,
23
+ proposed_start: int
24
+ ):
25
+ self.original_lines = original_lines
26
+ self.proposed_lines = proposed_lines
27
+ self.original_start = original_start
28
+ self.proposed_start = proposed_start
29
+ self.accepted = None # None = undecided, True = accepted, False = rejected
30
+
31
+ def get_context(self) -> str:
32
+ """Get a brief context description of this hunk."""
33
+ if self.proposed_lines:
34
+ first_line = self.proposed_lines[0].strip()
35
+ return first_line[:50] if len(first_line) > 50 else first_line
36
+ return "deletion"
37
+
38
+
39
+ class DiffViewer:
40
+ """Interactive diff viewer with multiple keybinding modes."""
41
+
42
+ def __init__(
43
+ self,
44
+ original_path: Path,
45
+ proposed_path: Path,
46
+ keybinding_mode: str = "auto",
47
+ console: Optional[Console] = None
48
+ ):
49
+ self.original_path = original_path
50
+ self.proposed_path = proposed_path
51
+ self.console = console or Console()
52
+
53
+ # Detect keybinding mode
54
+ if keybinding_mode == "auto":
55
+ self.keybinding_mode = self._detect_keybinding_mode()
56
+ else:
57
+ self.keybinding_mode = keybinding_mode
58
+
59
+ # Load files
60
+ with open(original_path) as f:
61
+ self.original_content = f.read()
62
+ self.original_lines = self.original_content.splitlines(keepends=True)
63
+
64
+ with open(proposed_path) as f:
65
+ self.proposed_content = f.read()
66
+ self.proposed_lines = self.proposed_content.splitlines(keepends=True)
67
+
68
+ # Generate hunks
69
+ self.hunks = self._generate_hunks()
70
+ self.current_hunk_idx = 0
71
+ self.filename = proposed_path.name
72
+
73
+ def _detect_keybinding_mode(self) -> str:
74
+ """Auto-detect keybinding preference from environment."""
75
+ # Check if user has vim/nvim in their environment
76
+ editor = os.environ.get("EDITOR", "").lower()
77
+ visual = os.environ.get("VISUAL", "").lower()
78
+
79
+ if "vim" in editor or "vim" in visual or "nvim" in editor or "nvim" in visual:
80
+ return "nvim"
81
+ return "fresh"
82
+
83
+ def _generate_hunks(self) -> List[Hunk]:
84
+ """Generate hunks from diff."""
85
+ hunks = []
86
+ differ = difflib.Differ()
87
+ diff = list(differ.compare(self.original_lines, self.proposed_lines))
88
+
89
+ i = 0
90
+ while i < len(diff):
91
+ # Find start of a hunk (lines that differ)
92
+ while i < len(diff) and diff[i].startswith(" "):
93
+ i += 1
94
+
95
+ if i >= len(diff):
96
+ break
97
+
98
+ # Collect the hunk
99
+ original_lines = []
100
+ proposed_lines = []
101
+ original_start = i
102
+ proposed_start = i
103
+
104
+ while i < len(diff) and not diff[i].startswith(" "):
105
+ line = diff[i]
106
+ if line.startswith("- "):
107
+ original_lines.append(line[2:])
108
+ elif line.startswith("+ "):
109
+ proposed_lines.append(line[2:])
110
+ elif line.startswith("? "):
111
+ # Skip hint lines
112
+ pass
113
+ i += 1
114
+
115
+ if original_lines or proposed_lines:
116
+ hunks.append(Hunk(
117
+ original_lines,
118
+ proposed_lines,
119
+ original_start,
120
+ proposed_start
121
+ ))
122
+
123
+ return hunks
124
+
125
+ def _get_keybindings(self) -> dict:
126
+ """Get keybindings based on mode."""
127
+ if self.keybinding_mode == "nvim":
128
+ return {
129
+ "accept": ["y", "a"],
130
+ "reject": ["n", "d"],
131
+ "edit": ["e", "c"],
132
+ "split": ["s"],
133
+ "quit": ["q", "ZZ"],
134
+ "accept_all": ["A"],
135
+ "reject_all": ["D"],
136
+ "undo": ["u"],
137
+ "help": ["?", "h"],
138
+ "next": ["j", "n"],
139
+ "prev": ["k", "p"],
140
+ "goto_first": ["gg"],
141
+ "goto_last": ["G"],
142
+ }
143
+ else: # fresh mode
144
+ return {
145
+ "accept": ["y", "Enter"],
146
+ "reject": ["n", "Backspace"],
147
+ "edit": ["e"],
148
+ "split": ["s"],
149
+ "quit": ["q", "Esc"],
150
+ "accept_all": ["Ctrl-A"],
151
+ "reject_all": ["Ctrl-R"],
152
+ "undo": ["Ctrl-Z"],
153
+ "help": ["F1", "?"],
154
+ "next": ["Down", "j"],
155
+ "prev": ["Up", "k"],
156
+ "goto_first": ["Home"],
157
+ "goto_last": ["End"],
158
+ }
159
+
160
+ def _show_help(self) -> None:
161
+ """Display help panel."""
162
+ kb = self._get_keybindings()
163
+ mode_name = "Neovim" if self.keybinding_mode == "nvim" else "Fresh"
164
+
165
+ table = Table(title=f"{mode_name} Keybindings", show_header=True)
166
+ table.add_column("Action", style="cyan")
167
+ table.add_column("Keys", style="green")
168
+
169
+ table.add_row("Accept hunk", " or ".join(kb["accept"]))
170
+ table.add_row("Reject hunk", " or ".join(kb["reject"]))
171
+ table.add_row("Edit hunk", " or ".join(kb["edit"]))
172
+ table.add_row("Split hunk", " or ".join(kb["split"]))
173
+ table.add_row("Next hunk", " or ".join(kb["next"]))
174
+ table.add_row("Previous hunk", " or ".join(kb["prev"]))
175
+ table.add_row("Accept all", " or ".join(kb["accept_all"]))
176
+ table.add_row("Reject all", " or ".join(kb["reject_all"]))
177
+ table.add_row("Undo", " or ".join(kb["undo"]))
178
+ table.add_row("Quit", " or ".join(kb["quit"]))
179
+ table.add_row("Help", " or ".join(kb["help"]))
180
+
181
+ self.console.print(table)
182
+ self.console.print("\n[dim]Press any key to continue...[/dim]")
183
+ self.console.input()
184
+
185
+ def _display_hunk(self, hunk: Hunk) -> None:
186
+ """Display a single hunk with syntax highlighting."""
187
+ self.console.clear()
188
+
189
+ # Title
190
+ mode_indicator = "🎹 Neovim" if self.keybinding_mode == "nvim" else "✨ Fresh"
191
+ self.console.print(
192
+ Panel(
193
+ f"{mode_indicator} Mode | {self.filename}",
194
+ title=f"Hunk {self.current_hunk_idx + 1}/{len(self.hunks)}",
195
+ border_style="blue"
196
+ )
197
+ )
198
+
199
+ # Show original (if any deletions)
200
+ if hunk.original_lines:
201
+ self.console.print("\n[bold red]━━━ Original (-):[/bold red]")
202
+ for line in hunk.original_lines:
203
+ self.console.print(f"[red]- {line}[/red]", end="")
204
+
205
+ # Show proposed (if any additions)
206
+ if hunk.proposed_lines:
207
+ self.console.print("\n[bold green]━━━ Proposed (+):[/bold green]")
208
+ for line in hunk.proposed_lines:
209
+ self.console.print(f"[green]+ {line}[/green]", end="")
210
+
211
+ # Context
212
+ context = hunk.get_context()
213
+ self.console.print(f"\n[dim]Context: {context}[/dim]")
214
+
215
+ # Status
216
+ status = "✓ Accepted" if hunk.accepted is True else "✗ Rejected" if hunk.accepted is False else "? Undecided"
217
+ self.console.print(f"Status: {status}\n")
218
+
219
+ def _show_prompt(self) -> None:
220
+ """Show action prompt based on keybinding mode."""
221
+ kb = self._get_keybindings()
222
+
223
+ if self.keybinding_mode == "nvim":
224
+ prompt = (
225
+ f"[cyan][y]es [n]o [e]dit [s]plit | "
226
+ f"[j]next [k]prev [q]uit [?]help[/cyan]"
227
+ )
228
+ else: # fresh
229
+ prompt = (
230
+ f"[cyan][y]es [n]o [e]dit [s]plit | "
231
+ f"[↓]next [↑]prev [q]uit [?]help[/cyan]"
232
+ )
233
+
234
+ self.console.print(prompt)
235
+
236
+ def run(self) -> Optional[str]:
237
+ """Run the interactive diff viewer.
238
+
239
+ Returns:
240
+ Final content with accepted changes or None if cancelled
241
+ """
242
+ if not self.hunks:
243
+ self.console.print("[yellow]No differences found[/yellow]")
244
+ return self.original_content
245
+
246
+ # Show initial help
247
+ self._show_help()
248
+
249
+ while self.current_hunk_idx < len(self.hunks):
250
+ hunk = self.hunks[self.current_hunk_idx]
251
+ self._display_hunk(hunk)
252
+ self._show_prompt()
253
+
254
+ # Get user input
255
+ choice = self.console.input("\nYour choice: ").strip().lower()
256
+
257
+ kb = self._get_keybindings()
258
+
259
+ # Process choice
260
+ if choice in kb["accept"]:
261
+ hunk.accepted = True
262
+ self.current_hunk_idx += 1
263
+ elif choice in kb["reject"]:
264
+ hunk.accepted = False
265
+ self.current_hunk_idx += 1
266
+ elif choice in kb["edit"]:
267
+ self.console.print("[yellow]Edit mode not yet implemented[/yellow]")
268
+ self.console.input("Press Enter to continue...")
269
+ elif choice in kb["split"]:
270
+ self.console.print("[yellow]Split mode not yet implemented[/yellow]")
271
+ self.console.input("Press Enter to continue...")
272
+ elif choice in kb["next"]:
273
+ if self.current_hunk_idx < len(self.hunks) - 1:
274
+ self.current_hunk_idx += 1
275
+ elif choice in kb["prev"]:
276
+ if self.current_hunk_idx > 0:
277
+ self.current_hunk_idx -= 1
278
+ elif choice in kb["accept_all"]:
279
+ for h in self.hunks[self.current_hunk_idx:]:
280
+ h.accepted = True
281
+ break
282
+ elif choice in kb["reject_all"]:
283
+ for h in self.hunks[self.current_hunk_idx:]:
284
+ h.accepted = False
285
+ break
286
+ elif choice in kb["quit"]:
287
+ self.console.print("[yellow]Quitting without applying changes[/yellow]")
288
+ return None
289
+ elif choice in kb["help"]:
290
+ self._show_help()
291
+ else:
292
+ self.console.print(f"[red]Unknown command: {choice}[/red]")
293
+ self.console.input("Press Enter to continue...")
294
+
295
+ # Apply accepted changes
296
+ return self._apply_changes()
297
+
298
+ def _apply_changes(self) -> str:
299
+ """Apply accepted changes and return final content."""
300
+ result_lines = list(self.original_lines)
301
+
302
+ # Apply hunks in reverse order to maintain line numbers
303
+ for hunk in reversed(self.hunks):
304
+ if hunk.accepted:
305
+ # Remove original lines
306
+ for _ in hunk.original_lines:
307
+ if hunk.original_start < len(result_lines):
308
+ result_lines.pop(hunk.original_start)
309
+
310
+ # Insert proposed lines
311
+ for i, line in enumerate(hunk.proposed_lines):
312
+ result_lines.insert(hunk.original_start + i, line)
313
+
314
+ return "".join(result_lines)
@@ -0,0 +1,117 @@
1
+ """TOON format utilities for token-efficient LLM communication."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ # Try to import toon-format, but make it optional
6
+ try:
7
+ from toon_format import encode as toon_encode, decode as toon_decode
8
+ TOON_AVAILABLE = True
9
+ except ImportError:
10
+ TOON_AVAILABLE = False
11
+ toon_encode = None
12
+ toon_decode = None
13
+
14
+
15
+ def is_toon_available() -> bool:
16
+ """Check if TOON format support is available."""
17
+ return TOON_AVAILABLE
18
+
19
+
20
+ def to_toon(data: Any) -> str:
21
+ """
22
+ Convert Python data to TOON format.
23
+
24
+ Args:
25
+ data: Python dict, list, or primitive to encode
26
+
27
+ Returns:
28
+ TOON-formatted string
29
+
30
+ Raises:
31
+ ImportError: If toon-format is not installed
32
+ """
33
+ if not TOON_AVAILABLE:
34
+ raise ImportError(
35
+ "TOON format support not installed. "
36
+ "Install with: pip install claude-dev-cli[toon]"
37
+ )
38
+
39
+ return toon_encode(data)
40
+
41
+
42
+ def from_toon(toon_str: str) -> Any:
43
+ """
44
+ Convert TOON format back to Python data.
45
+
46
+ Args:
47
+ toon_str: TOON-formatted string
48
+
49
+ Returns:
50
+ Python dict, list, or primitive
51
+
52
+ Raises:
53
+ ImportError: If toon-format is not installed
54
+ """
55
+ if not TOON_AVAILABLE:
56
+ raise ImportError(
57
+ "TOON format support not installed. "
58
+ "Install with: pip install claude-dev-cli[toon]"
59
+ )
60
+
61
+ return toon_decode(toon_str)
62
+
63
+
64
+ def format_for_llm(data: Any, use_toon: bool = True) -> str:
65
+ """
66
+ Format data for LLM consumption, preferring TOON if available.
67
+
68
+ Args:
69
+ data: Data to format
70
+ use_toon: Whether to use TOON format if available (default: True)
71
+
72
+ Returns:
73
+ Formatted string (TOON if available and requested, else JSON)
74
+ """
75
+ import json
76
+
77
+ if use_toon and TOON_AVAILABLE:
78
+ try:
79
+ return to_toon(data)
80
+ except Exception:
81
+ # Fall back to JSON if TOON encoding fails
82
+ pass
83
+
84
+ return json.dumps(data, indent=2)
85
+
86
+
87
+ def auto_detect_format(content: str) -> tuple[str, Any]:
88
+ """
89
+ Auto-detect if content is TOON or JSON and decode accordingly.
90
+
91
+ Args:
92
+ content: String content to decode
93
+
94
+ Returns:
95
+ Tuple of (format_name, decoded_data)
96
+
97
+ Raises:
98
+ ValueError: If content cannot be parsed as either format
99
+ """
100
+ import json
101
+
102
+ # Try TOON first if available
103
+ if TOON_AVAILABLE:
104
+ try:
105
+ data = from_toon(content)
106
+ return ("toon", data)
107
+ except Exception:
108
+ pass
109
+
110
+ # Try JSON
111
+ try:
112
+ data = json.loads(content)
113
+ return ("json", data)
114
+ except json.JSONDecodeError:
115
+ pass
116
+
117
+ raise ValueError("Content is neither valid TOON nor JSON")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.1.0
3
+ Version: 0.3.0
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
@@ -26,11 +26,14 @@ Requires-Dist: anthropic>=0.18.0
26
26
  Requires-Dist: click>=8.1.0
27
27
  Requires-Dist: rich>=13.0.0
28
28
  Requires-Dist: pydantic>=2.0.0
29
+ Provides-Extra: toon
30
+ Requires-Dist: toon-format>=0.9.0; extra == "toon"
29
31
  Provides-Extra: dev
30
32
  Requires-Dist: pytest>=7.0.0; extra == "dev"
31
33
  Requires-Dist: black>=23.0.0; extra == "dev"
32
34
  Requires-Dist: ruff>=0.1.0; extra == "dev"
33
35
  Requires-Dist: mypy>=1.0.0; extra == "dev"
36
+ Requires-Dist: toon-format>=0.9.0; extra == "dev"
34
37
  Dynamic: license-file
35
38
 
36
39
  # Claude Dev CLI
@@ -63,12 +66,27 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
63
66
  - **Interactive**: Chat mode with conversation history
64
67
  - **Streaming**: Real-time responses
65
68
 
69
+ ### 🎒 TOON Format Support (Optional)
70
+ - **Token Reduction**: 30-60% fewer tokens than JSON
71
+ - **Cost Savings**: Reduce API costs significantly
72
+ - **Format Conversion**: JSON ↔ TOON with CLI tools
73
+ - **Auto-fallback**: Works without TOON installed
74
+
66
75
  ## Installation
67
76
 
77
+ ### Basic Installation
78
+
68
79
  ```bash
69
80
  pip install claude-dev-cli
70
81
  ```
71
82
 
83
+ ### With TOON Support (Recommended for Cost Savings)
84
+
85
+ ```bash
86
+ # Install with TOON format support for 30-60% token reduction
87
+ pip install claude-dev-cli[toon]
88
+ ```
89
+
72
90
  ## Quick Start
73
91
 
74
92
  ### 1. Set Up API Keys
@@ -141,6 +159,28 @@ cdc usage --days 7
141
159
  cdc usage --api client
142
160
  ```
143
161
 
162
+ ### 5. TOON Format (Optional - Reduces Tokens by 30-60%)
163
+
164
+ ```bash
165
+ # Check if TOON is installed
166
+ cdc toon info
167
+
168
+ # Convert JSON to TOON
169
+ echo '{"users": [{"id": 1, "name": "Alice"}]}' | cdc toon encode
170
+ # Output:
171
+ # users[1]{id,name}:
172
+ # 1,Alice
173
+
174
+ # Convert file
175
+ cdc toon encode data.json -o data.toon
176
+
177
+ # Convert TOON back to JSON
178
+ cdc toon decode data.toon -o data.json
179
+
180
+ # Use in workflows
181
+ cat large_data.json | cdc toon encode | cdc ask "analyze this data"
182
+ ```
183
+
144
184
  ## Configuration
145
185
 
146
186
  ### Global Configuration
@@ -0,0 +1,19 @@
1
+ claude_dev_cli/__init__.py,sha256=2ulyIQ3E-s6wBTKyeXAlqHMVA73zUGdaaNUsFiJ-nqs,469
2
+ claude_dev_cli/cli.py,sha256=ekJpBU3d0k9DrE5VoJdKGNgPBsmdB4415up_Yhx_fw0,16174
3
+ claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
4
+ claude_dev_cli/config.py,sha256=YwJjVkW9S1O_iq_2O6YCjYtuFWUCmP18zA7esKDwkKU,5776
5
+ claude_dev_cli/core.py,sha256=97rR9BuNfnhJxFrd7dTdApGyPh6MeGNArcRmaiOY69I,4443
6
+ claude_dev_cli/templates.py,sha256=lKxH943ySfUKgyHaWa4W3LVv91SgznKgajRtSRp_4UY,2260
7
+ claude_dev_cli/toon_utils.py,sha256=S3px2UvmNEaltmTa5K-h21n2c0CPvYjZc9mc7kHGqNQ,2828
8
+ claude_dev_cli/usage.py,sha256=32rs0_dUn6ihha3vCfT3rwnvel_-sED7jvLpO7gu-KQ,7446
9
+ claude_dev_cli/plugins/__init__.py,sha256=BdiZlylBzEgnwK2tuEdn8cITxhAZRVbTnDbWhdDhgqs,1340
10
+ claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-c,1417
11
+ claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
12
+ claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
13
+ claude_dev_cli/plugins/diff_editor/viewer.py,sha256=wm8TG-aOrCV0f1NaL-Jwi93UaksfApESQpjmPPRIQTs,11597
14
+ claude_dev_cli-0.3.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
15
+ claude_dev_cli-0.3.0.dist-info/METADATA,sha256=lR6Z4dFk6UaXzPHBwLX9hi7eiKEHaI9I0Hcg1yVCR9w,10325
16
+ claude_dev_cli-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ claude_dev_cli-0.3.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
18
+ claude_dev_cli-0.3.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
19
+ claude_dev_cli-0.3.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- claude_dev_cli/__init__.py,sha256=O2wq4c3zAUKOJ6liwykkHMqEyhhasrv69ybnAmP9BNU,469
2
- claude_dev_cli/cli.py,sha256=s5nYAvNQDYXfNXFs9NbVRib3EasqXMZayJNekl3JRX4,11574
3
- claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
4
- claude_dev_cli/config.py,sha256=YwJjVkW9S1O_iq_2O6YCjYtuFWUCmP18zA7esKDwkKU,5776
5
- claude_dev_cli/core.py,sha256=97rR9BuNfnhJxFrd7dTdApGyPh6MeGNArcRmaiOY69I,4443
6
- claude_dev_cli/templates.py,sha256=lKxH943ySfUKgyHaWa4W3LVv91SgznKgajRtSRp_4UY,2260
7
- claude_dev_cli/usage.py,sha256=32rs0_dUn6ihha3vCfT3rwnvel_-sED7jvLpO7gu-KQ,7446
8
- claude_dev_cli-0.1.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
9
- claude_dev_cli-0.1.0.dist-info/METADATA,sha256=mdYAz3MjH7qEZ-aOBsM3Jml8NNJUQz2HJvsSQZMioMM,9313
10
- claude_dev_cli-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- claude_dev_cli-0.1.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
12
- claude_dev_cli-0.1.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
13
- claude_dev_cli-0.1.0.dist-info/RECORD,,