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.
- claude_dev_cli/__init__.py +1 -1
- claude_dev_cli/cli.py +132 -0
- claude_dev_cli/plugins/__init__.py +42 -0
- claude_dev_cli/plugins/base.py +53 -0
- claude_dev_cli/plugins/diff_editor/__init__.py +3 -0
- claude_dev_cli/plugins/diff_editor/plugin.py +98 -0
- claude_dev_cli/plugins/diff_editor/viewer.py +314 -0
- claude_dev_cli/toon_utils.py +117 -0
- {claude_dev_cli-0.1.0.dist-info → claude_dev_cli-0.3.0.dist-info}/METADATA +41 -1
- claude_dev_cli-0.3.0.dist-info/RECORD +19 -0
- claude_dev_cli-0.1.0.dist-info/RECORD +0 -13
- {claude_dev_cli-0.1.0.dist-info → claude_dev_cli-0.3.0.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.1.0.dist-info → claude_dev_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.1.0.dist-info → claude_dev_cli-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.1.0.dist-info → claude_dev_cli-0.3.0.dist-info}/top_level.txt +0 -0
claude_dev_cli/__init__.py
CHANGED
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,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.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|