claude-dev-cli 0.6.0__py3-none-any.whl → 0.7.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.

@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.6.0"
12
+ __version__ = "0.7.0"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
claude_dev_cli/cli.py CHANGED
@@ -447,12 +447,14 @@ def generate() -> None:
447
447
  @click.argument('file_path', type=click.Path(exists=True))
448
448
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
449
449
  @click.option('-a', '--api', help='API config to use')
450
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
450
451
  @click.pass_context
451
452
  def gen_tests(
452
453
  ctx: click.Context,
453
454
  file_path: str,
454
455
  output: Optional[str],
455
- api: Optional[str]
456
+ api: Optional[str],
457
+ interactive: bool
456
458
  ) -> None:
457
459
  """Generate pytest tests for a Python file."""
458
460
  console = ctx.obj['console']
@@ -461,11 +463,48 @@ def gen_tests(
461
463
  with console.status("[bold blue]Generating tests..."):
462
464
  result = generate_tests(file_path, api_config_name=api)
463
465
 
466
+ if interactive:
467
+ # Show initial result
468
+ console.print("\n[bold]Initial Tests:[/bold]\n")
469
+ console.print(result)
470
+
471
+ # Interactive refinement loop
472
+ client = ClaudeClient(api_config_name=api)
473
+ conversation_context = [result]
474
+
475
+ while True:
476
+ console.print("\n[dim]Commands: 'save' to save and exit, 'exit' to discard, or ask for changes[/dim]")
477
+ user_input = console.input("[cyan]You:[/cyan] ").strip()
478
+
479
+ if user_input.lower() == 'exit':
480
+ console.print("[yellow]Discarded changes[/yellow]")
481
+ return
482
+
483
+ if user_input.lower() == 'save':
484
+ result = conversation_context[-1]
485
+ break
486
+
487
+ if not user_input:
488
+ continue
489
+
490
+ # Get refinement
491
+ refinement_prompt = f"Previous tests:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated tests."
492
+
493
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
494
+ response_parts = []
495
+ for chunk in client.call_streaming(refinement_prompt):
496
+ console.print(chunk, end='')
497
+ response_parts.append(chunk)
498
+ console.print()
499
+
500
+ result = ''.join(response_parts)
501
+ conversation_context.append(result)
502
+
464
503
  if output:
465
504
  with open(output, 'w') as f:
466
505
  f.write(result)
467
- console.print(f"[green]✓[/green] Tests saved to: {output}")
468
- else:
506
+ console.print(f"\n[green]✓[/green] Tests saved to: {output}")
507
+ elif not interactive:
469
508
  console.print(result)
470
509
 
471
510
  except Exception as e:
@@ -477,12 +516,14 @@ def gen_tests(
477
516
  @click.argument('file_path', type=click.Path(exists=True))
478
517
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
479
518
  @click.option('-a', '--api', help='API config to use')
519
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
480
520
  @click.pass_context
481
521
  def gen_docs(
482
522
  ctx: click.Context,
483
523
  file_path: str,
484
524
  output: Optional[str],
485
- api: Optional[str]
525
+ api: Optional[str],
526
+ interactive: bool
486
527
  ) -> None:
487
528
  """Generate documentation for a Python file."""
488
529
  console = ctx.obj['console']
@@ -491,11 +532,46 @@ def gen_docs(
491
532
  with console.status("[bold blue]Generating documentation..."):
492
533
  result = generate_docs(file_path, api_config_name=api)
493
534
 
535
+ if interactive:
536
+ console.print("\n[bold]Initial Documentation:[/bold]\n")
537
+ md = Markdown(result)
538
+ console.print(md)
539
+
540
+ client = ClaudeClient(api_config_name=api)
541
+ conversation_context = [result]
542
+
543
+ while True:
544
+ console.print("\n[dim]Commands: 'save' to save and exit, 'exit' to discard, or ask for changes[/dim]")
545
+ user_input = console.input("[cyan]You:[/cyan] ").strip()
546
+
547
+ if user_input.lower() == 'exit':
548
+ console.print("[yellow]Discarded changes[/yellow]")
549
+ return
550
+
551
+ if user_input.lower() == 'save':
552
+ result = conversation_context[-1]
553
+ break
554
+
555
+ if not user_input:
556
+ continue
557
+
558
+ refinement_prompt = f"Previous documentation:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated documentation."
559
+
560
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
561
+ response_parts = []
562
+ for chunk in client.call_streaming(refinement_prompt):
563
+ console.print(chunk, end='')
564
+ response_parts.append(chunk)
565
+ console.print()
566
+
567
+ result = ''.join(response_parts)
568
+ conversation_context.append(result)
569
+
494
570
  if output:
495
571
  with open(output, 'w') as f:
496
572
  f.write(result)
497
- console.print(f"[green]✓[/green] Documentation saved to: {output}")
498
- else:
573
+ console.print(f"\n[green]✓[/green] Documentation saved to: {output}")
574
+ elif not interactive:
499
575
  md = Markdown(result)
500
576
  console.print(md)
501
577
 
@@ -507,11 +583,13 @@ def gen_docs(
507
583
  @main.command('review')
508
584
  @click.argument('file_path', type=click.Path(exists=True))
509
585
  @click.option('-a', '--api', help='API config to use')
586
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive follow-up questions')
510
587
  @click.pass_context
511
588
  def review(
512
589
  ctx: click.Context,
513
590
  file_path: str,
514
- api: Optional[str]
591
+ api: Optional[str],
592
+ interactive: bool
515
593
  ) -> None:
516
594
  """Review code for bugs and improvements."""
517
595
  console = ctx.obj['console']
@@ -522,6 +600,29 @@ def review(
522
600
 
523
601
  md = Markdown(result)
524
602
  console.print(md)
603
+
604
+ if interactive:
605
+ client = ClaudeClient(api_config_name=api)
606
+ with open(file_path, 'r') as f:
607
+ file_content = f.read()
608
+
609
+ console.print("\n[dim]Ask follow-up questions about the review, or 'exit' to quit[/dim]")
610
+
611
+ while True:
612
+ user_input = console.input("\n[cyan]You:[/cyan] ").strip()
613
+
614
+ if user_input.lower() == 'exit':
615
+ break
616
+
617
+ if not user_input:
618
+ continue
619
+
620
+ follow_up_prompt = f"Code review:\n\n{result}\n\nOriginal code:\n\n{file_content}\n\nUser question: {user_input}"
621
+
622
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
623
+ for chunk in client.call_streaming(follow_up_prompt):
624
+ console.print(chunk, end='')
625
+ console.print()
525
626
 
526
627
  except Exception as e:
527
628
  console.print(f"[red]Error: {e}[/red]")
@@ -567,12 +668,14 @@ def debug(
567
668
  @click.argument('file_path', type=click.Path(exists=True))
568
669
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
569
670
  @click.option('-a', '--api', help='API config to use')
671
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
570
672
  @click.pass_context
571
673
  def refactor(
572
674
  ctx: click.Context,
573
675
  file_path: str,
574
676
  output: Optional[str],
575
- api: Optional[str]
677
+ api: Optional[str],
678
+ interactive: bool
576
679
  ) -> None:
577
680
  """Suggest refactoring improvements."""
578
681
  console = ctx.obj['console']
@@ -581,11 +684,46 @@ def refactor(
581
684
  with console.status("[bold blue]Analyzing code..."):
582
685
  result = refactor_code(file_path, api_config_name=api)
583
686
 
687
+ if interactive:
688
+ console.print("\n[bold]Initial Refactoring:[/bold]\n")
689
+ md = Markdown(result)
690
+ console.print(md)
691
+
692
+ client = ClaudeClient(api_config_name=api)
693
+ conversation_context = [result]
694
+
695
+ while True:
696
+ console.print("\n[dim]Commands: 'save' to save and exit, 'exit' to discard, or ask for changes[/dim]")
697
+ user_input = console.input("[cyan]You:[/cyan] ").strip()
698
+
699
+ if user_input.lower() == 'exit':
700
+ console.print("[yellow]Discarded changes[/yellow]")
701
+ return
702
+
703
+ if user_input.lower() == 'save':
704
+ result = conversation_context[-1]
705
+ break
706
+
707
+ if not user_input:
708
+ continue
709
+
710
+ refinement_prompt = f"Previous refactoring:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated refactoring suggestions."
711
+
712
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
713
+ response_parts = []
714
+ for chunk in client.call_streaming(refinement_prompt):
715
+ console.print(chunk, end='')
716
+ response_parts.append(chunk)
717
+ console.print()
718
+
719
+ result = ''.join(response_parts)
720
+ conversation_context.append(result)
721
+
584
722
  if output:
585
723
  with open(output, 'w') as f:
586
724
  f.write(result)
587
- console.print(f"[green]✓[/green] Refactored code saved to: {output}")
588
- else:
725
+ console.print(f"\n[green]✓[/green] Refactored code saved to: {output}")
726
+ elif not interactive:
589
727
  md = Markdown(result)
590
728
  console.print(md)
591
729
 
@@ -957,5 +1095,221 @@ def template_use(ctx: click.Context, name: str, api: Optional[str], model: Optio
957
1095
  sys.exit(1)
958
1096
 
959
1097
 
1098
+ @main.group()
1099
+ def warp() -> None:
1100
+ """Warp terminal integration."""
1101
+ pass
1102
+
1103
+
1104
+ @warp.command('export-workflows')
1105
+ @click.option('-o', '--output', type=click.Path(), help='Output directory')
1106
+ @click.pass_context
1107
+ def warp_export_workflows(ctx: click.Context, output: Optional[str]) -> None:
1108
+ """Export Warp workflows for claude-dev-cli commands."""
1109
+ console = ctx.obj['console']
1110
+
1111
+ try:
1112
+ from claude_dev_cli.warp_integration import export_builtin_workflows
1113
+
1114
+ config = Config()
1115
+ output_dir = Path(output) if output else config.config_dir / "warp" / "workflows"
1116
+
1117
+ created_files = export_builtin_workflows(output_dir)
1118
+
1119
+ console.print(f"[green]✓[/green] Exported {len(created_files)} Warp workflows to:")
1120
+ console.print(f" {output_dir}")
1121
+ console.print("\n[bold]Workflows:[/bold]")
1122
+ for file in created_files:
1123
+ console.print(f" • {file.name}")
1124
+
1125
+ except Exception as e:
1126
+ console.print(f"[red]Error: {e}[/red]")
1127
+ sys.exit(1)
1128
+
1129
+
1130
+ @warp.command('export-launch-configs')
1131
+ @click.option('-o', '--output', type=click.Path(), help='Output file path')
1132
+ @click.pass_context
1133
+ def warp_export_launch_configs(ctx: click.Context, output: Optional[str]) -> None:
1134
+ """Export Warp launch configurations."""
1135
+ console = ctx.obj['console']
1136
+
1137
+ try:
1138
+ from claude_dev_cli.warp_integration import export_launch_configs
1139
+
1140
+ config = Config()
1141
+ output_path = Path(output) if output else config.config_dir / "warp" / "launch_configs.json"
1142
+
1143
+ export_launch_configs(output_path)
1144
+
1145
+ console.print(f"[green]✓[/green] Exported Warp launch configurations to:")
1146
+ console.print(f" {output_path}")
1147
+
1148
+ except Exception as e:
1149
+ console.print(f"[red]Error: {e}[/red]")
1150
+ sys.exit(1)
1151
+
1152
+
1153
+ @main.group()
1154
+ def workflow() -> None:
1155
+ """Manage and run workflows."""
1156
+ pass
1157
+
1158
+
1159
+ @workflow.command('run')
1160
+ @click.argument('workflow_file', type=click.Path(exists=True))
1161
+ @click.option('--var', '-v', multiple=True, help='Set variables (key=value)')
1162
+ @click.pass_context
1163
+ def workflow_run(
1164
+ ctx: click.Context,
1165
+ workflow_file: str,
1166
+ var: tuple
1167
+ ) -> None:
1168
+ """Run a workflow from YAML file."""
1169
+ console = ctx.obj['console']
1170
+
1171
+ # Parse variables
1172
+ variables = {}
1173
+ for v in var:
1174
+ if '=' in v:
1175
+ key, value = v.split('=', 1)
1176
+ variables[key] = value
1177
+
1178
+ try:
1179
+ from claude_dev_cli.workflows import WorkflowEngine
1180
+
1181
+ engine = WorkflowEngine(console=console)
1182
+ workflow_path = Path(workflow_file)
1183
+
1184
+ context = engine.execute(workflow_path, initial_vars=variables)
1185
+
1186
+ # Show summary
1187
+ if context.step_results:
1188
+ console.print("\n[bold]Results Summary:[/bold]")
1189
+ for step_name, result in context.step_results.items():
1190
+ status = "[green]✓[/green]" if result.success else "[red]✗[/red]"
1191
+ console.print(f"{status} {step_name}")
1192
+
1193
+ except Exception as e:
1194
+ console.print(f"[red]Error: {e}[/red]")
1195
+ sys.exit(1)
1196
+
1197
+
1198
+ @workflow.command('list')
1199
+ @click.pass_context
1200
+ def workflow_list(ctx: click.Context) -> None:
1201
+ """List available workflows."""
1202
+ console = ctx.obj['console']
1203
+ config = Config()
1204
+ workflow_dir = config.config_dir / "workflows"
1205
+
1206
+ from claude_dev_cli.workflows import list_workflows
1207
+ workflows = list_workflows(workflow_dir)
1208
+
1209
+ if not workflows:
1210
+ console.print("[yellow]No workflows found.[/yellow]")
1211
+ console.print(f"\nCreate workflows in: {workflow_dir}")
1212
+ return
1213
+
1214
+ from rich.table import Table
1215
+
1216
+ table = Table(show_header=True, header_style="bold magenta")
1217
+ table.add_column("Name", style="cyan")
1218
+ table.add_column("Steps", style="yellow")
1219
+ table.add_column("Description")
1220
+
1221
+ for wf in workflows:
1222
+ table.add_row(
1223
+ wf['name'],
1224
+ str(wf['steps']),
1225
+ wf['description']
1226
+ )
1227
+
1228
+ console.print(table)
1229
+ console.print(f"\n[dim]Workflow directory: {workflow_dir}[/dim]")
1230
+
1231
+
1232
+ @workflow.command('show')
1233
+ @click.argument('workflow_file', type=click.Path(exists=True))
1234
+ @click.pass_context
1235
+ def workflow_show(ctx: click.Context, workflow_file: str) -> None:
1236
+ """Show workflow details."""
1237
+ console = ctx.obj['console']
1238
+
1239
+ try:
1240
+ from claude_dev_cli.workflows import WorkflowEngine
1241
+
1242
+ engine = WorkflowEngine(console=console)
1243
+ workflow = engine.load_workflow(Path(workflow_file))
1244
+
1245
+ console.print(Panel(
1246
+ f"[bold]{workflow.get('name', 'Unnamed')}[/bold]\n\n"
1247
+ f"[dim]{workflow.get('description', 'No description')}[/dim]\n\n"
1248
+ f"Steps: [yellow]{len(workflow.get('steps', []))}[/yellow]",
1249
+ title="Workflow Info",
1250
+ border_style="blue"
1251
+ ))
1252
+
1253
+ # Show steps
1254
+ steps = workflow.get('steps', [])
1255
+ if steps:
1256
+ console.print("\n[bold]Steps:[/bold]\n")
1257
+ for i, step in enumerate(steps, 1):
1258
+ step_name = step.get('name', f'step-{i}')
1259
+ step_type = 'command' if 'command' in step else 'shell' if 'shell' in step else 'set'
1260
+ console.print(f" {i}. [cyan]{step_name}[/cyan] ({step_type})")
1261
+
1262
+ if step.get('approval_required'):
1263
+ console.print(f" [yellow]⚠ Requires approval[/yellow]")
1264
+ if 'if' in step:
1265
+ console.print(f" [dim]Condition: {step['if']}[/dim]")
1266
+
1267
+ except Exception as e:
1268
+ console.print(f"[red]Error: {e}[/red]")
1269
+ sys.exit(1)
1270
+
1271
+
1272
+ @workflow.command('validate')
1273
+ @click.argument('workflow_file', type=click.Path(exists=True))
1274
+ @click.pass_context
1275
+ def workflow_validate(ctx: click.Context, workflow_file: str) -> None:
1276
+ """Validate workflow syntax."""
1277
+ console = ctx.obj['console']
1278
+
1279
+ try:
1280
+ from claude_dev_cli.workflows import WorkflowEngine
1281
+
1282
+ engine = WorkflowEngine(console=console)
1283
+ workflow = engine.load_workflow(Path(workflow_file))
1284
+
1285
+ # Basic validation
1286
+ errors = []
1287
+
1288
+ if 'name' not in workflow:
1289
+ errors.append("Missing 'name' field")
1290
+
1291
+ if 'steps' not in workflow:
1292
+ errors.append("Missing 'steps' field")
1293
+ elif not isinstance(workflow['steps'], list):
1294
+ errors.append("'steps' must be a list")
1295
+ else:
1296
+ for i, step in enumerate(workflow['steps'], 1):
1297
+ if not any(k in step for k in ['command', 'shell', 'set']):
1298
+ errors.append(f"Step {i}: Must have 'command', 'shell', or 'set'")
1299
+
1300
+ if errors:
1301
+ console.print("[red]✗ Validation failed:[/red]\n")
1302
+ for error in errors:
1303
+ console.print(f" • {error}")
1304
+ sys.exit(1)
1305
+ else:
1306
+ console.print(f"[green]✓[/green] Workflow is valid: {workflow.get('name')}")
1307
+ console.print(f" Steps: {len(workflow.get('steps', []))}")
1308
+
1309
+ except Exception as e:
1310
+ console.print(f"[red]Error: {e}[/red]")
1311
+ sys.exit(1)
1312
+
1313
+
960
1314
  if __name__ == '__main__':
961
1315
  main(obj={})
@@ -0,0 +1,243 @@
1
+ """Warp terminal integration for enhanced output formatting."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import yaml
8
+
9
+
10
+ def format_as_warp_block(
11
+ content: str,
12
+ title: Optional[str] = None,
13
+ language: Optional[str] = None,
14
+ actions: Optional[List[Dict[str, str]]] = None
15
+ ) -> str:
16
+ """Format content as a Warp block with optional actions.
17
+
18
+ Warp blocks support special formatting and click-to-run actions.
19
+ """
20
+ block_parts = []
21
+
22
+ if title:
23
+ block_parts.append(f"### {title}")
24
+ block_parts.append("")
25
+
26
+ # Add content with language if code block
27
+ if language:
28
+ block_parts.append(f"```{language}")
29
+ block_parts.append(content)
30
+ block_parts.append("```")
31
+ else:
32
+ block_parts.append(content)
33
+
34
+ # Add actions if provided
35
+ if actions:
36
+ block_parts.append("")
37
+ block_parts.append("**Actions:**")
38
+ for action in actions:
39
+ label = action.get('label', 'Run')
40
+ command = action.get('command', '')
41
+ block_parts.append(f"- [{label}](`{command}`)")
42
+
43
+ return "\n".join(block_parts)
44
+
45
+
46
+ def generate_warp_workflow(
47
+ workflow_name: str,
48
+ commands: List[Dict[str, str]],
49
+ output_path: Optional[Path] = None
50
+ ) -> str:
51
+ """Generate a Warp workflow file from command list.
52
+
53
+ Warp workflows allow users to execute predefined command sequences.
54
+ """
55
+ workflow = {
56
+ "name": workflow_name,
57
+ "command": "://" + workflow_name.lower().replace(" ", "-"),
58
+ "tags": ["claude-dev-cli"],
59
+ "description": f"Generated from claude-dev-cli",
60
+ "arguments": [],
61
+ "source_specs": [
62
+ {
63
+ "type": "command",
64
+ "command": cmd.get('command', ''),
65
+ "description": cmd.get('description', '')
66
+ }
67
+ for cmd in commands
68
+ ]
69
+ }
70
+
71
+ workflow_yaml = yaml.dump(workflow, default_flow_style=False, sort_keys=False)
72
+
73
+ if output_path:
74
+ output_path.write_text(workflow_yaml)
75
+
76
+ return workflow_yaml
77
+
78
+
79
+ def export_builtin_workflows(output_dir: Path) -> List[Path]:
80
+ """Export built-in Warp workflows for common claude-dev-cli tasks."""
81
+ output_dir.mkdir(parents=True, exist_ok=True)
82
+
83
+ workflows = [
84
+ {
85
+ "name": "Code Review Workflow",
86
+ "commands": [
87
+ {
88
+ "command": "cdc review {{file}}",
89
+ "description": "Review code for issues"
90
+ },
91
+ {
92
+ "command": "cdc review {{file}} --interactive",
93
+ "description": "Review with follow-up questions"
94
+ }
95
+ ]
96
+ },
97
+ {
98
+ "name": "Test Generation Workflow",
99
+ "commands": [
100
+ {
101
+ "command": "cdc generate tests {{file}} -o tests/test_{{file}}",
102
+ "description": "Generate tests"
103
+ },
104
+ {
105
+ "command": "pytest tests/test_{{file}}",
106
+ "description": "Run tests"
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ "name": "Refactor Workflow",
112
+ "commands": [
113
+ {
114
+ "command": "cdc refactor {{file}} --interactive",
115
+ "description": "Get refactoring suggestions"
116
+ },
117
+ {
118
+ "command": "cdc review {{file}}",
119
+ "description": "Review changes"
120
+ },
121
+ {
122
+ "command": "git add {{file}} && cdc git commit",
123
+ "description": "Commit changes"
124
+ }
125
+ ]
126
+ },
127
+ {
128
+ "name": "Debug Workflow",
129
+ "commands": [
130
+ {
131
+ "command": "python {{file}} 2>&1 | cdc debug",
132
+ "description": "Run and debug errors"
133
+ },
134
+ {
135
+ "command": "cdc debug -f {{file}} -e \"{{error}}\"",
136
+ "description": "Debug specific error"
137
+ }
138
+ ]
139
+ }
140
+ ]
141
+
142
+ created_files = []
143
+ for workflow in workflows:
144
+ filename = workflow['name'].lower().replace(' ', '-') + '.yaml'
145
+ filepath = output_dir / filename
146
+ generate_warp_workflow(
147
+ workflow['name'],
148
+ workflow['commands'],
149
+ filepath
150
+ )
151
+ created_files.append(filepath)
152
+
153
+ return created_files
154
+
155
+
156
+ def format_code_review_for_warp(review_output: str, file_path: str) -> str:
157
+ """Format code review output as Warp block with actions."""
158
+ actions = [
159
+ {
160
+ "label": "Review with follow-up",
161
+ "command": f"cdc review {file_path} --interactive"
162
+ },
163
+ {
164
+ "label": "Refactor",
165
+ "command": f"cdc refactor {file_path} --interactive"
166
+ }
167
+ ]
168
+
169
+ return format_as_warp_block(
170
+ content=review_output,
171
+ title="Code Review",
172
+ actions=actions
173
+ )
174
+
175
+
176
+ def format_test_generation_for_warp(test_output: str, file_path: str) -> str:
177
+ """Format test generation output as Warp block with actions."""
178
+ test_file = f"tests/test_{Path(file_path).name}"
179
+
180
+ actions = [
181
+ {
182
+ "label": "Save tests",
183
+ "command": f"cdc generate tests {file_path} -o {test_file}"
184
+ },
185
+ {
186
+ "label": "Run tests",
187
+ "command": f"pytest {test_file}"
188
+ }
189
+ ]
190
+
191
+ return format_as_warp_block(
192
+ content=test_output,
193
+ title="Generated Tests",
194
+ language="python",
195
+ actions=actions
196
+ )
197
+
198
+
199
+ def create_warp_launch_config(
200
+ name: str,
201
+ command: str,
202
+ cwd: Optional[str] = None,
203
+ env: Optional[Dict[str, str]] = None
204
+ ) -> Dict[str, Any]:
205
+ """Create a Warp launch configuration.
206
+
207
+ Launch configs allow quick environment setup in Warp.
208
+ """
209
+ config = {
210
+ "name": name,
211
+ "command": command,
212
+ "type": "terminal"
213
+ }
214
+
215
+ if cwd:
216
+ config["cwd"] = cwd
217
+
218
+ if env:
219
+ config["env"] = env
220
+
221
+ return config
222
+
223
+
224
+ def export_launch_configs(output_path: Path) -> None:
225
+ """Export Warp launch configurations for claude-dev-cli."""
226
+ configs = [
227
+ create_warp_launch_config(
228
+ name="Claude Dev CLI - Interactive",
229
+ command="cdc interactive"
230
+ ),
231
+ create_warp_launch_config(
232
+ name="Claude Dev CLI - Review Mode",
233
+ command="cdc review"
234
+ ),
235
+ create_warp_launch_config(
236
+ name="Claude Dev CLI - Test Generation",
237
+ command="cdc generate tests"
238
+ )
239
+ ]
240
+
241
+ output_path.parent.mkdir(parents=True, exist_ok=True)
242
+ with open(output_path, 'w') as f:
243
+ json.dump({"launch_configurations": configs}, f, indent=2)
@@ -0,0 +1,340 @@
1
+ """Workflow execution engine for chaining AI operations."""
2
+
3
+ import re
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Union
7
+ from dataclasses import dataclass, field
8
+
9
+ import yaml
10
+ from rich.console import Console
11
+
12
+
13
+ @dataclass
14
+ class StepResult:
15
+ """Result from executing a workflow step."""
16
+ success: bool
17
+ output: str
18
+ error: Optional[str] = None
19
+ metadata: Dict[str, Any] = field(default_factory=dict)
20
+
21
+
22
+ @dataclass
23
+ class WorkflowContext:
24
+ """Context passed between workflow steps."""
25
+ variables: Dict[str, Any] = field(default_factory=dict)
26
+ step_results: Dict[str, StepResult] = field(default_factory=dict)
27
+
28
+
29
+ class WorkflowEngine:
30
+ """Execute workflow definitions with step chaining."""
31
+
32
+ def __init__(self, console: Optional[Console] = None):
33
+ self.console = console or Console()
34
+
35
+ def load_workflow(self, path: Path) -> Dict[str, Any]:
36
+ """Load workflow from YAML file."""
37
+ with open(path, 'r') as f:
38
+ return yaml.safe_load(f)
39
+
40
+ def execute(
41
+ self,
42
+ workflow: Union[Dict[str, Any], Path],
43
+ initial_vars: Optional[Dict[str, Any]] = None
44
+ ) -> WorkflowContext:
45
+ """Execute a workflow definition."""
46
+ # Load from file if path provided
47
+ if isinstance(workflow, Path):
48
+ workflow = self.load_workflow(workflow)
49
+
50
+ # Initialize context
51
+ context = WorkflowContext(variables=initial_vars or {})
52
+
53
+ # Extract workflow metadata
54
+ name = workflow.get('name', 'Unnamed Workflow')
55
+ description = workflow.get('description', '')
56
+ steps = workflow.get('steps', [])
57
+
58
+ self.console.print(f"\n[bold cyan]Running workflow:[/bold cyan] {name}")
59
+ if description:
60
+ self.console.print(f"[dim]{description}[/dim]")
61
+
62
+ # Execute steps
63
+ for i, step in enumerate(steps, 1):
64
+ step_name = step.get('name', f'step-{i}')
65
+
66
+ # Check conditional
67
+ if 'if' in step:
68
+ condition = self._evaluate_condition(step['if'], context)
69
+ if not condition:
70
+ self.console.print(f"[yellow]↷[/yellow] Skipping step {i}: {step_name} (condition false)")
71
+ continue
72
+
73
+ self.console.print(f"\n[bold]Step {i}:[/bold] {step_name}")
74
+
75
+ # Check for approval gate
76
+ if step.get('approval_required', False):
77
+ if not self._request_approval(step_name):
78
+ self.console.print("[yellow]Workflow stopped by user[/yellow]")
79
+ break
80
+
81
+ # Execute step
82
+ try:
83
+ result = self._execute_step(step, context)
84
+ context.step_results[step_name] = result
85
+
86
+ if result.success:
87
+ self.console.print(f"[green]✓[/green] {step_name} completed")
88
+ else:
89
+ self.console.print(f"[red]✗[/red] {step_name} failed: {result.error}")
90
+
91
+ # Check if workflow should continue on error
92
+ if not step.get('continue_on_error', False):
93
+ self.console.print("[red]Workflow stopped due to error[/red]")
94
+ break
95
+
96
+ except Exception as e:
97
+ self.console.print(f"[red]Error in step {step_name}: {e}[/red]")
98
+ if not step.get('continue_on_error', False):
99
+ break
100
+
101
+ self.console.print("\n[bold green]Workflow completed[/bold green]")
102
+ return context
103
+
104
+ def _execute_step(self, step: Dict[str, Any], context: WorkflowContext) -> StepResult:
105
+ """Execute a single workflow step."""
106
+ # Determine step type
107
+ if 'command' in step:
108
+ return self._execute_command_step(step, context)
109
+ elif 'shell' in step:
110
+ return self._execute_shell_step(step, context)
111
+ elif 'set' in step:
112
+ return self._execute_set_step(step, context)
113
+ else:
114
+ return StepResult(
115
+ success=False,
116
+ output="",
117
+ error="Unknown step type"
118
+ )
119
+
120
+ def _execute_command_step(self, step: Dict[str, Any], context: WorkflowContext) -> StepResult:
121
+ """Execute a cdc command step."""
122
+ command = step['command']
123
+ args = step.get('args', {})
124
+
125
+ # Interpolate variables in args
126
+ interpolated_args = self._interpolate_variables(args, context)
127
+
128
+ # Import here to avoid circular dependency
129
+ from claude_dev_cli.commands import (
130
+ generate_tests, code_review, debug_code,
131
+ generate_docs, refactor_code, git_commit_message
132
+ )
133
+
134
+ # Map commands to functions
135
+ command_map = {
136
+ 'generate tests': generate_tests,
137
+ 'review': code_review,
138
+ 'debug': debug_code,
139
+ 'generate docs': generate_docs,
140
+ 'refactor': refactor_code,
141
+ 'git commit': git_commit_message,
142
+ }
143
+
144
+ if command not in command_map:
145
+ return StepResult(
146
+ success=False,
147
+ output="",
148
+ error=f"Unknown command: {command}"
149
+ )
150
+
151
+ try:
152
+ func = command_map[command]
153
+
154
+ # Build function arguments
155
+ func_args = {}
156
+ if 'file' in interpolated_args:
157
+ func_args['file_path'] = interpolated_args['file']
158
+ if 'error' in interpolated_args:
159
+ func_args['error_message'] = interpolated_args['error']
160
+ if 'api' in interpolated_args:
161
+ func_args['api_config_name'] = interpolated_args['api']
162
+
163
+ # Execute
164
+ result = func(**func_args)
165
+
166
+ # Store output in context for next steps
167
+ if 'output_var' in step:
168
+ context.variables[step['output_var']] = result
169
+
170
+ return StepResult(
171
+ success=True,
172
+ output=result,
173
+ metadata={'command': command}
174
+ )
175
+
176
+ except Exception as e:
177
+ return StepResult(
178
+ success=False,
179
+ output="",
180
+ error=str(e)
181
+ )
182
+
183
+ def _execute_shell_step(self, step: Dict[str, Any], context: WorkflowContext) -> StepResult:
184
+ """Execute a shell command step."""
185
+ command = step['shell']
186
+
187
+ # Interpolate variables
188
+ command = self._interpolate_string(command, context)
189
+
190
+ try:
191
+ result = subprocess.run(
192
+ command,
193
+ shell=True,
194
+ capture_output=True,
195
+ text=True,
196
+ check=False
197
+ )
198
+
199
+ success = result.returncode == 0
200
+ output = result.stdout or result.stderr
201
+
202
+ # Store output in context
203
+ if 'output_var' in step:
204
+ context.variables[step['output_var']] = output.strip()
205
+
206
+ return StepResult(
207
+ success=success,
208
+ output=output,
209
+ error=result.stderr if not success else None,
210
+ metadata={'returncode': result.returncode}
211
+ )
212
+
213
+ except Exception as e:
214
+ return StepResult(
215
+ success=False,
216
+ output="",
217
+ error=str(e)
218
+ )
219
+
220
+ def _execute_set_step(self, step: Dict[str, Any], context: WorkflowContext) -> StepResult:
221
+ """Execute a variable assignment step."""
222
+ var_name = step['set']
223
+ value = step.get('value', '')
224
+
225
+ # Interpolate value
226
+ value = self._interpolate_value(value, context)
227
+
228
+ context.variables[var_name] = value
229
+
230
+ return StepResult(
231
+ success=True,
232
+ output=str(value),
233
+ metadata={'variable': var_name}
234
+ )
235
+
236
+ def _interpolate_variables(
237
+ self,
238
+ data: Union[Dict, List, str, Any],
239
+ context: WorkflowContext
240
+ ) -> Any:
241
+ """Recursively interpolate variables in data structures."""
242
+ if isinstance(data, dict):
243
+ return {k: self._interpolate_variables(v, context) for k, v in data.items()}
244
+ elif isinstance(data, list):
245
+ return [self._interpolate_variables(item, context) for item in data]
246
+ elif isinstance(data, str):
247
+ return self._interpolate_string(data, context)
248
+ else:
249
+ return data
250
+
251
+ def _interpolate_string(self, text: str, context: WorkflowContext) -> str:
252
+ """Interpolate {{variable}} placeholders in string."""
253
+ def replace(match: re.Match) -> str:
254
+ var_path = match.group(1)
255
+ return str(self._resolve_variable(var_path, context))
256
+
257
+ return re.sub(r'\{\{([^}]+)\}\}', replace, text)
258
+
259
+ def _interpolate_value(self, value: Any, context: WorkflowContext) -> Any:
260
+ """Interpolate a single value."""
261
+ if isinstance(value, str):
262
+ return self._interpolate_string(value, context)
263
+ return value
264
+
265
+ def _resolve_variable(self, var_path: str, context: WorkflowContext) -> Any:
266
+ """Resolve a variable path like 'step1.output' or 'vars.filename'."""
267
+ parts = var_path.strip().split('.')
268
+
269
+ # Check step results first
270
+ if len(parts) >= 2 and parts[0] in context.step_results:
271
+ step_result = context.step_results[parts[0]]
272
+ if parts[1] == 'output':
273
+ return step_result.output
274
+ elif parts[1] == 'success':
275
+ return step_result.success
276
+ elif parts[1] == 'error':
277
+ return step_result.error or ''
278
+
279
+ # Check variables
280
+ if parts[0] in context.variables:
281
+ value = context.variables[parts[0]]
282
+ # Support nested access for dicts
283
+ for part in parts[1:]:
284
+ if isinstance(value, dict):
285
+ value = value.get(part, '')
286
+ else:
287
+ return ''
288
+ return value
289
+
290
+ return f'{{{{{var_path}}}}}' # Return placeholder if not found
291
+
292
+ def _evaluate_condition(self, condition: str, context: WorkflowContext) -> bool:
293
+ """Evaluate a simple condition expression."""
294
+ # Interpolate variables
295
+ evaluated = self._interpolate_string(condition, context)
296
+
297
+ # Simple boolean evaluation
298
+ if evaluated.lower() in ('true', '1', 'yes'):
299
+ return True
300
+ if evaluated.lower() in ('false', '0', 'no', ''):
301
+ return False
302
+
303
+ # Try to evaluate as Python expression (limited to safe operations)
304
+ try:
305
+ # Only allow simple comparisons
306
+ if any(op in evaluated for op in ['==', '!=', '>', '<', '>=', '<=']):
307
+ return eval(evaluated, {"__builtins__": {}}, {})
308
+ except Exception:
309
+ pass
310
+
311
+ return bool(evaluated)
312
+
313
+ def _request_approval(self, step_name: str) -> bool:
314
+ """Request user approval to proceed."""
315
+ self.console.print(f"\n[yellow]⚠ Approval required for:[/yellow] {step_name}")
316
+ response = self.console.input("[cyan]Proceed? (y/n):[/cyan] ").strip().lower()
317
+ return response in ('y', 'yes')
318
+
319
+
320
+ def list_workflows(workflow_dir: Path) -> List[Dict[str, str]]:
321
+ """List available workflows in a directory."""
322
+ if not workflow_dir.exists():
323
+ return []
324
+
325
+ workflows = []
326
+ for file in workflow_dir.glob('*.yaml'):
327
+ try:
328
+ with open(file, 'r') as f:
329
+ data = yaml.safe_load(f)
330
+
331
+ workflows.append({
332
+ 'name': data.get('name', file.stem),
333
+ 'description': data.get('description', ''),
334
+ 'file': str(file),
335
+ 'steps': len(data.get('steps', []))
336
+ })
337
+ except Exception:
338
+ continue
339
+
340
+ return workflows
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -28,6 +28,7 @@ Requires-Dist: rich>=13.0.0
28
28
  Requires-Dist: pydantic>=2.0.0
29
29
  Requires-Dist: keyring>=24.0.0
30
30
  Requires-Dist: cryptography>=41.0.0
31
+ Requires-Dist: pyyaml>=6.0.0
31
32
  Provides-Extra: toon
32
33
  Requires-Dist: toon-format>=0.9.0; extra == "toon"
33
34
  Provides-Extra: plugins
@@ -145,18 +146,30 @@ cdc ask -a client "generate tests for this function"
145
146
  # Generate tests
146
147
  cdc generate tests mymodule.py -o tests/test_mymodule.py
147
148
 
149
+ # Generate tests with interactive refinement
150
+ cdc generate tests mymodule.py --interactive
151
+
148
152
  # Code review
149
153
  cdc review mymodule.py
150
154
 
155
+ # Code review with interactive follow-up questions
156
+ cdc review mymodule.py --interactive
157
+
151
158
  # Debug errors
152
159
  python script.py 2>&1 | cdc debug
153
160
 
154
161
  # Generate documentation
155
162
  cdc generate docs mymodule.py
156
163
 
164
+ # Generate docs with interactive refinement
165
+ cdc generate docs mymodule.py --interactive
166
+
157
167
  # Refactor suggestions
158
168
  cdc refactor legacy_code.py
159
169
 
170
+ # Refactor with interactive refinement
171
+ cdc refactor legacy_code.py --interactive
172
+
160
173
  # Git commit message
161
174
  git add .
162
175
  cdc git commit
@@ -1,5 +1,5 @@
1
- claude_dev_cli/__init__.py,sha256=8WXldNvP94ff1oD0MSxSxb5h0bEF-eJZyVHPopW8gq8,469
2
- claude_dev_cli/cli.py,sha256=KJbUDQl_hA7jgLGxXQs01MW6inzevQk6d7_mUvvAAB8,31296
1
+ claude_dev_cli/__init__.py,sha256=sRGmW8jYKXWyBKqr2_-3AornNC32eNj4sESrXfoTUoQ,469
2
+ claude_dev_cli/cli.py,sha256=feTRI1fQoDPJiB7JM45ZU2I1jEjXX643191H_U0rMio,44612
3
3
  claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
4
4
  claude_dev_cli/config.py,sha256=RGX0sKplHUsrJJmU-4FuWWjoTbQVgWaMT8DgRUofrR4,8134
5
5
  claude_dev_cli/core.py,sha256=yaLjEixDvPzvUy4fJ2UB7nMpPPLyKACjR-RuM-1OQBY,4780
@@ -9,14 +9,16 @@ claude_dev_cli/template_manager.py,sha256=ZFXOtRIoB6hpf8kLSF9TWJfvUPJt9b-PyEv3qT
9
9
  claude_dev_cli/templates.py,sha256=lKxH943ySfUKgyHaWa4W3LVv91SgznKgajRtSRp_4UY,2260
10
10
  claude_dev_cli/toon_utils.py,sha256=S3px2UvmNEaltmTa5K-h21n2c0CPvYjZc9mc7kHGqNQ,2828
11
11
  claude_dev_cli/usage.py,sha256=32rs0_dUn6ihha3vCfT3rwnvel_-sED7jvLpO7gu-KQ,7446
12
+ claude_dev_cli/warp_integration.py,sha256=PDufAJecl7uN0DGz6XW4uDSEeu7Ssg53DOIFKm-AXVg,6909
13
+ claude_dev_cli/workflows.py,sha256=WpLq9I_0MmDsJIbCi9-f662JVyn8iKTs1KZ50w-GlZU,12202
12
14
  claude_dev_cli/plugins/__init__.py,sha256=BdiZlylBzEgnwK2tuEdn8cITxhAZRVbTnDbWhdDhgqs,1340
13
15
  claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-c,1417
14
16
  claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
15
17
  claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
16
18
  claude_dev_cli/plugins/diff_editor/viewer.py,sha256=1IOXIKw_01ppJx5C1dQt9Kr6U1TdAHT8_iUT5r_q0NM,17169
17
- claude_dev_cli-0.6.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
18
- claude_dev_cli-0.6.0.dist-info/METADATA,sha256=d1aptyTDwcmtYPMrSgTk4j5pHCPgVriDnyjx--JjDCk,12984
19
- claude_dev_cli-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- claude_dev_cli-0.6.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
21
- claude_dev_cli-0.6.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
22
- claude_dev_cli-0.6.0.dist-info/RECORD,,
19
+ claude_dev_cli-0.7.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
20
+ claude_dev_cli-0.7.0.dist-info/METADATA,sha256=YIN5U4urelKIEFjhd-s2EyHsPb-qEvhhqDwOek2hjPw,13364
21
+ claude_dev_cli-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ claude_dev_cli-0.7.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
23
+ claude_dev_cli-0.7.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
24
+ claude_dev_cli-0.7.0.dist-info/RECORD,,