claude-dev-cli 0.16.1__py3-none-any.whl → 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-dev-cli might be problematic. Click here for more details.
- claude_dev_cli/__init__.py +1 -1
- claude_dev_cli/cli.py +424 -0
- claude_dev_cli/logging/__init__.py +6 -0
- claude_dev_cli/logging/logger.py +84 -0
- claude_dev_cli/logging/markdown_logger.py +131 -0
- claude_dev_cli/notifications/__init__.py +6 -0
- claude_dev_cli/notifications/notifier.py +69 -0
- claude_dev_cli/notifications/ntfy.py +87 -0
- claude_dev_cli/project/__init__.py +10 -0
- claude_dev_cli/project/bug_tracker.py +458 -0
- claude_dev_cli/project/context_gatherer.py +535 -0
- claude_dev_cli/project/executor.py +370 -0
- claude_dev_cli/tickets/__init__.py +7 -0
- claude_dev_cli/tickets/backend.py +229 -0
- claude_dev_cli/tickets/markdown.py +309 -0
- claude_dev_cli/tickets/repo_tickets.py +361 -0
- claude_dev_cli/vcs/__init__.py +6 -0
- claude_dev_cli/vcs/git.py +172 -0
- claude_dev_cli/vcs/manager.py +90 -0
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/METADATA +600 -10
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/RECORD +25 -8
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.16.1.dist-info → claude_dev_cli-0.18.0.dist-info}/top_level.txt +0 -0
claude_dev_cli/__init__.py
CHANGED
claude_dev_cli/cli.py
CHANGED
|
@@ -2873,5 +2873,429 @@ def workflow_validate(ctx: click.Context, workflow_file: str) -> None:
|
|
|
2873
2873
|
sys.exit(1)
|
|
2874
2874
|
|
|
2875
2875
|
|
|
2876
|
+
# ============================================================================
|
|
2877
|
+
# PROJECT MANAGEMENT COMMANDS (v0.17.0)
|
|
2878
|
+
# ============================================================================
|
|
2879
|
+
|
|
2880
|
+
@main.group()
|
|
2881
|
+
def ticket() -> None:
|
|
2882
|
+
"""Execute tickets from external systems (repo-tickets, Jira, etc.)."""
|
|
2883
|
+
pass
|
|
2884
|
+
|
|
2885
|
+
|
|
2886
|
+
@ticket.command('execute')
|
|
2887
|
+
@click.argument('ticket_id')
|
|
2888
|
+
@click.option('--backend', type=click.Choice(['repo-tickets', 'markdown']), default='markdown',
|
|
2889
|
+
help='Ticket backend to use')
|
|
2890
|
+
@click.option('--notify', is_flag=True, help='Send notifications')
|
|
2891
|
+
@click.option('--commit', is_flag=True, help='Auto-commit changes')
|
|
2892
|
+
@click.option('--no-context', is_flag=True, help='Skip codebase context gathering')
|
|
2893
|
+
@click.option('-a', '--api', help='API config for AI generation')
|
|
2894
|
+
@click.option('-m', '--model', help='Model to use')
|
|
2895
|
+
@click.pass_context
|
|
2896
|
+
def ticket_execute(
|
|
2897
|
+
ctx: click.Context,
|
|
2898
|
+
ticket_id: str,
|
|
2899
|
+
backend: str,
|
|
2900
|
+
notify: bool,
|
|
2901
|
+
commit: bool,
|
|
2902
|
+
no_context: bool,
|
|
2903
|
+
api: Optional[str],
|
|
2904
|
+
model: Optional[str]
|
|
2905
|
+
) -> None:
|
|
2906
|
+
"""Execute a ticket: fetch → analyze → generate code → tests → commit.
|
|
2907
|
+
|
|
2908
|
+
By default, gathers codebase context (dependencies, frameworks, similar code)
|
|
2909
|
+
before generating code. Use --no-context to skip this step for faster execution.
|
|
2910
|
+
|
|
2911
|
+
Examples:
|
|
2912
|
+
cdc ticket execute TASK-123
|
|
2913
|
+
cdc ticket execute JIRA-456 --backend repo-tickets --commit --notify
|
|
2914
|
+
cdc ticket execute TASK-789 --no-context # Skip context gathering
|
|
2915
|
+
"""
|
|
2916
|
+
console = ctx.obj['console']
|
|
2917
|
+
|
|
2918
|
+
try:
|
|
2919
|
+
from claude_dev_cli.project import TicketExecutor
|
|
2920
|
+
from claude_dev_cli.tickets import MarkdownBackend, RepoTicketsBackend
|
|
2921
|
+
from claude_dev_cli.logging import MarkdownLogger
|
|
2922
|
+
from claude_dev_cli.notifications import NtfyNotifier
|
|
2923
|
+
from claude_dev_cli.vcs import GitManager
|
|
2924
|
+
from claude_dev_cli.core import ClaudeClient
|
|
2925
|
+
|
|
2926
|
+
# Initialize backend
|
|
2927
|
+
if backend == 'repo-tickets':
|
|
2928
|
+
ticket_backend = RepoTicketsBackend()
|
|
2929
|
+
else:
|
|
2930
|
+
ticket_backend = MarkdownBackend()
|
|
2931
|
+
|
|
2932
|
+
if not ticket_backend.connect():
|
|
2933
|
+
console.print(f"[red]Failed to connect to {backend} backend[/red]")
|
|
2934
|
+
sys.exit(1)
|
|
2935
|
+
|
|
2936
|
+
# Initialize components
|
|
2937
|
+
logger = MarkdownLogger()
|
|
2938
|
+
logger.init(f"Ticket Execution - {ticket_id}")
|
|
2939
|
+
|
|
2940
|
+
notifier = NtfyNotifier(topic="cdc-tickets") if notify else None
|
|
2941
|
+
vcs = GitManager() if commit else None
|
|
2942
|
+
|
|
2943
|
+
ai_client = ClaudeClient(api_config_name=api) if api else None
|
|
2944
|
+
|
|
2945
|
+
# Create executor
|
|
2946
|
+
executor = TicketExecutor(
|
|
2947
|
+
ticket_backend=ticket_backend,
|
|
2948
|
+
ai_client=ai_client,
|
|
2949
|
+
logger=logger,
|
|
2950
|
+
notifier=notifier,
|
|
2951
|
+
vcs=vcs,
|
|
2952
|
+
auto_commit=commit,
|
|
2953
|
+
gather_context=not no_context # Gather context unless --no-context flag is set
|
|
2954
|
+
)
|
|
2955
|
+
|
|
2956
|
+
console.print(f"[cyan]Executing ticket:[/cyan] {ticket_id}")
|
|
2957
|
+
if not no_context:
|
|
2958
|
+
console.print("[dim]Context gathering: ENABLED[/dim]")
|
|
2959
|
+
else:
|
|
2960
|
+
console.print("[dim]Context gathering: DISABLED[/dim]")
|
|
2961
|
+
console.print()
|
|
2962
|
+
|
|
2963
|
+
with console.status(f"[bold blue]Processing {ticket_id}..."):
|
|
2964
|
+
success = executor.execute_ticket(ticket_id)
|
|
2965
|
+
|
|
2966
|
+
if success:
|
|
2967
|
+
console.print(f"\n[green]✅ Ticket {ticket_id} completed successfully![/green]")
|
|
2968
|
+
console.print(f"\n[dim]📊 Check .cdc-logs/progress.md for details[/dim]")
|
|
2969
|
+
else:
|
|
2970
|
+
console.print(f"\n[red]❌ Ticket {ticket_id} execution failed[/red]")
|
|
2971
|
+
sys.exit(1)
|
|
2972
|
+
|
|
2973
|
+
except Exception as e:
|
|
2974
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
2975
|
+
sys.exit(1)
|
|
2976
|
+
|
|
2977
|
+
|
|
2978
|
+
@main.group()
|
|
2979
|
+
def tickets() -> None:
|
|
2980
|
+
"""Manage tickets (create, list, update)."""
|
|
2981
|
+
pass
|
|
2982
|
+
|
|
2983
|
+
|
|
2984
|
+
@tickets.command('create')
|
|
2985
|
+
@click.argument('title')
|
|
2986
|
+
@click.option('--description', '-d', help='Ticket description')
|
|
2987
|
+
@click.option('--priority', type=click.Choice(['critical', 'high', 'medium', 'low']),
|
|
2988
|
+
default='medium', help='Priority level')
|
|
2989
|
+
@click.option('--type', 'ticket_type', type=click.Choice(['feature', 'bug', 'refactor', 'test', 'doc']),
|
|
2990
|
+
default='feature', help='Ticket type')
|
|
2991
|
+
@click.option('--backend', type=click.Choice(['repo-tickets', 'markdown']), default='markdown',
|
|
2992
|
+
help='Ticket backend')
|
|
2993
|
+
@click.pass_context
|
|
2994
|
+
def tickets_create(
|
|
2995
|
+
ctx: click.Context,
|
|
2996
|
+
title: str,
|
|
2997
|
+
description: Optional[str],
|
|
2998
|
+
priority: str,
|
|
2999
|
+
ticket_type: str,
|
|
3000
|
+
backend: str
|
|
3001
|
+
) -> None:
|
|
3002
|
+
"""Create a new ticket.
|
|
3003
|
+
|
|
3004
|
+
Examples:
|
|
3005
|
+
cdc tickets create "Add user auth" --priority high
|
|
3006
|
+
cdc tickets create "Fix login bug" --type bug --backend repo-tickets
|
|
3007
|
+
"""
|
|
3008
|
+
console = ctx.obj['console']
|
|
3009
|
+
|
|
3010
|
+
try:
|
|
3011
|
+
from claude_dev_cli.tickets import MarkdownBackend, RepoTicketsBackend
|
|
3012
|
+
|
|
3013
|
+
if backend == 'repo-tickets':
|
|
3014
|
+
ticket_backend = RepoTicketsBackend()
|
|
3015
|
+
else:
|
|
3016
|
+
ticket_backend = MarkdownBackend()
|
|
3017
|
+
|
|
3018
|
+
ticket_backend.connect()
|
|
3019
|
+
|
|
3020
|
+
ticket = ticket_backend.create_task(
|
|
3021
|
+
story_id=None,
|
|
3022
|
+
title=title,
|
|
3023
|
+
description=description or "",
|
|
3024
|
+
priority=priority,
|
|
3025
|
+
ticket_type=ticket_type
|
|
3026
|
+
)
|
|
3027
|
+
|
|
3028
|
+
console.print(f"[green]✅ Created ticket:[/green] {ticket.id}")
|
|
3029
|
+
console.print(f" Title: {ticket.title}")
|
|
3030
|
+
console.print(f" Priority: {ticket.priority}")
|
|
3031
|
+
console.print(f" Type: {ticket.ticket_type}")
|
|
3032
|
+
|
|
3033
|
+
except Exception as e:
|
|
3034
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
3035
|
+
sys.exit(1)
|
|
3036
|
+
|
|
3037
|
+
|
|
3038
|
+
@tickets.command('list')
|
|
3039
|
+
@click.option('--status', help='Filter by status')
|
|
3040
|
+
@click.option('--backend', type=click.Choice(['repo-tickets', 'markdown']), default='markdown')
|
|
3041
|
+
@click.pass_context
|
|
3042
|
+
def tickets_list(ctx: click.Context, status: Optional[str], backend: str) -> None:
|
|
3043
|
+
"""List tickets."""
|
|
3044
|
+
console = ctx.obj['console']
|
|
3045
|
+
|
|
3046
|
+
try:
|
|
3047
|
+
from claude_dev_cli.tickets import MarkdownBackend, RepoTicketsBackend
|
|
3048
|
+
from rich.table import Table
|
|
3049
|
+
|
|
3050
|
+
if backend == 'repo-tickets':
|
|
3051
|
+
ticket_backend = RepoTicketsBackend()
|
|
3052
|
+
else:
|
|
3053
|
+
ticket_backend = MarkdownBackend()
|
|
3054
|
+
|
|
3055
|
+
ticket_backend.connect()
|
|
3056
|
+
tickets = ticket_backend.list_tickets(status=status)
|
|
3057
|
+
|
|
3058
|
+
if not tickets:
|
|
3059
|
+
console.print("[yellow]No tickets found[/yellow]")
|
|
3060
|
+
return
|
|
3061
|
+
|
|
3062
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
3063
|
+
table.add_column("ID", style="cyan")
|
|
3064
|
+
table.add_column("Title")
|
|
3065
|
+
table.add_column("Status", style="yellow")
|
|
3066
|
+
table.add_column("Priority", style="red")
|
|
3067
|
+
table.add_column("Type", style="blue")
|
|
3068
|
+
|
|
3069
|
+
for ticket in tickets:
|
|
3070
|
+
table.add_row(
|
|
3071
|
+
ticket.id,
|
|
3072
|
+
ticket.title[:50],
|
|
3073
|
+
ticket.status,
|
|
3074
|
+
ticket.priority,
|
|
3075
|
+
ticket.ticket_type
|
|
3076
|
+
)
|
|
3077
|
+
|
|
3078
|
+
console.print(table)
|
|
3079
|
+
console.print(f"\n[dim]Total: {len(tickets)} ticket(s)[/dim]")
|
|
3080
|
+
|
|
3081
|
+
except Exception as e:
|
|
3082
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
3083
|
+
sys.exit(1)
|
|
3084
|
+
|
|
3085
|
+
|
|
3086
|
+
@main.group()
|
|
3087
|
+
def bug() -> None:
|
|
3088
|
+
"""Report and triage bugs."""
|
|
3089
|
+
pass
|
|
3090
|
+
|
|
3091
|
+
|
|
3092
|
+
@bug.command('report')
|
|
3093
|
+
@click.option('--title', '-t', required=True, help='Bug title')
|
|
3094
|
+
@click.option('--description', '-d', required=True, help='Bug description')
|
|
3095
|
+
@click.option('--expected', '-e', required=True, help='Expected behavior')
|
|
3096
|
+
@click.option('--actual', '-a', required=True, help='Actual behavior')
|
|
3097
|
+
@click.option('--steps', '-s', multiple=True, help='Steps to reproduce')
|
|
3098
|
+
@click.option('--environment', help='Environment (production/staging/dev)')
|
|
3099
|
+
@click.option('--severity', type=click.Choice(['critical', 'high', 'medium', 'low', 'trivial']),
|
|
3100
|
+
help='Override auto-triage severity')
|
|
3101
|
+
@click.option('--no-triage', is_flag=True, help='Skip auto-triage')
|
|
3102
|
+
@click.option('--backend', type=click.Choice(['repo-tickets', 'markdown']), default='markdown')
|
|
3103
|
+
@click.pass_context
|
|
3104
|
+
def bug_report(
|
|
3105
|
+
ctx: click.Context,
|
|
3106
|
+
title: str,
|
|
3107
|
+
description: str,
|
|
3108
|
+
expected: str,
|
|
3109
|
+
actual: str,
|
|
3110
|
+
steps: tuple,
|
|
3111
|
+
environment: Optional[str],
|
|
3112
|
+
severity: Optional[str],
|
|
3113
|
+
no_triage: bool,
|
|
3114
|
+
backend: str
|
|
3115
|
+
) -> None:
|
|
3116
|
+
"""Report a bug with automatic triage.
|
|
3117
|
+
|
|
3118
|
+
Examples:
|
|
3119
|
+
cdc bug report \\
|
|
3120
|
+
--title "App crashes on startup" \\
|
|
3121
|
+
--description "Application fails to launch" \\
|
|
3122
|
+
--expected "App should start normally" \\
|
|
3123
|
+
--actual "App crashes immediately" \\
|
|
3124
|
+
--steps "Open app" --steps "Wait 2 seconds" \\
|
|
3125
|
+
--environment production
|
|
3126
|
+
"""
|
|
3127
|
+
console = ctx.obj['console']
|
|
3128
|
+
|
|
3129
|
+
try:
|
|
3130
|
+
from claude_dev_cli.project import BugTriageSystem, BugReport, BugSeverity
|
|
3131
|
+
from claude_dev_cli.tickets import MarkdownBackend, RepoTicketsBackend
|
|
3132
|
+
from claude_dev_cli.notifications import NtfyNotifier
|
|
3133
|
+
|
|
3134
|
+
if backend == 'repo-tickets':
|
|
3135
|
+
ticket_backend = RepoTicketsBackend()
|
|
3136
|
+
else:
|
|
3137
|
+
ticket_backend = MarkdownBackend()
|
|
3138
|
+
|
|
3139
|
+
ticket_backend.connect()
|
|
3140
|
+
|
|
3141
|
+
# Create bug report
|
|
3142
|
+
bug = BugReport(
|
|
3143
|
+
id=None,
|
|
3144
|
+
title=title,
|
|
3145
|
+
description=description,
|
|
3146
|
+
steps_to_reproduce=list(steps) if steps else [],
|
|
3147
|
+
expected_behavior=expected,
|
|
3148
|
+
actual_behavior=actual,
|
|
3149
|
+
environment=environment,
|
|
3150
|
+
severity=BugSeverity(severity) if severity else None
|
|
3151
|
+
)
|
|
3152
|
+
|
|
3153
|
+
# Initialize triage system
|
|
3154
|
+
triage = BugTriageSystem(
|
|
3155
|
+
ticket_backend=ticket_backend,
|
|
3156
|
+
notifier=NtfyNotifier(topic="cdc-bugs")
|
|
3157
|
+
)
|
|
3158
|
+
|
|
3159
|
+
console.print(f"[cyan]Submitting bug report:[/cyan] {title}\n")
|
|
3160
|
+
|
|
3161
|
+
with console.status("[bold blue]Triaging bug...") if not no_triage else console:
|
|
3162
|
+
ticket = triage.submit_bug(bug, auto_triage=not no_triage)
|
|
3163
|
+
|
|
3164
|
+
console.print(f"\n[green]✅ Bug reported:[/green] {ticket.id}")
|
|
3165
|
+
console.print(f" Title: {bug.title}")
|
|
3166
|
+
|
|
3167
|
+
if bug.severity:
|
|
3168
|
+
severity_color = {
|
|
3169
|
+
"critical": "red",
|
|
3170
|
+
"high": "yellow",
|
|
3171
|
+
"medium": "blue",
|
|
3172
|
+
"low": "cyan",
|
|
3173
|
+
"trivial": "dim"
|
|
3174
|
+
}.get(bug.severity.value, "white")
|
|
3175
|
+
console.print(f" Severity: [{severity_color}]{bug.severity.value.upper()}[/{severity_color}]")
|
|
3176
|
+
|
|
3177
|
+
if bug.category:
|
|
3178
|
+
console.print(f" Category: {bug.category.value}")
|
|
3179
|
+
|
|
3180
|
+
if bug.priority:
|
|
3181
|
+
console.print(f" Priority: {bug.priority}")
|
|
3182
|
+
|
|
3183
|
+
except Exception as e:
|
|
3184
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
3185
|
+
sys.exit(1)
|
|
3186
|
+
|
|
3187
|
+
|
|
3188
|
+
@main.group()
|
|
3189
|
+
def log() -> None:
|
|
3190
|
+
"""Manage progress logs."""
|
|
3191
|
+
pass
|
|
3192
|
+
|
|
3193
|
+
|
|
3194
|
+
@log.command('init')
|
|
3195
|
+
@click.argument('project_name')
|
|
3196
|
+
@click.pass_context
|
|
3197
|
+
def log_init(ctx: click.Context, project_name: str) -> None:
|
|
3198
|
+
"""Initialize progress logging."""
|
|
3199
|
+
console = ctx.obj['console']
|
|
3200
|
+
|
|
3201
|
+
try:
|
|
3202
|
+
from claude_dev_cli.logging import MarkdownLogger
|
|
3203
|
+
|
|
3204
|
+
logger = MarkdownLogger()
|
|
3205
|
+
logger.init(project_name)
|
|
3206
|
+
|
|
3207
|
+
console.print(f"[green]✅ Initialized logging for:[/green] {project_name}")
|
|
3208
|
+
console.print(f"[dim]Log file: .cdc-logs/progress.md[/dim]")
|
|
3209
|
+
|
|
3210
|
+
except Exception as e:
|
|
3211
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
3212
|
+
sys.exit(1)
|
|
3213
|
+
|
|
3214
|
+
|
|
3215
|
+
@log.command('entry')
|
|
3216
|
+
@click.argument('message')
|
|
3217
|
+
@click.option('--ticket', help='Associated ticket ID')
|
|
3218
|
+
@click.option('--level', type=click.Choice(['info', 'success', 'error', 'warning']),
|
|
3219
|
+
default='info', help='Log level')
|
|
3220
|
+
@click.pass_context
|
|
3221
|
+
def log_entry(
|
|
3222
|
+
ctx: click.Context,
|
|
3223
|
+
message: str,
|
|
3224
|
+
ticket: Optional[str],
|
|
3225
|
+
level: str
|
|
3226
|
+
) -> None:
|
|
3227
|
+
"""Add a log entry."""
|
|
3228
|
+
console = ctx.obj['console']
|
|
3229
|
+
|
|
3230
|
+
try:
|
|
3231
|
+
from claude_dev_cli.logging import MarkdownLogger
|
|
3232
|
+
|
|
3233
|
+
logger = MarkdownLogger()
|
|
3234
|
+
logger.log(message, ticket_id=ticket, level=level)
|
|
3235
|
+
|
|
3236
|
+
console.print(f"[green]✅ Logged:[/green] {message}")
|
|
3237
|
+
|
|
3238
|
+
except Exception as e:
|
|
3239
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
3240
|
+
sys.exit(1)
|
|
3241
|
+
|
|
3242
|
+
|
|
3243
|
+
@log.command('report')
|
|
3244
|
+
@click.pass_context
|
|
3245
|
+
def log_report(ctx: click.Context) -> None:
|
|
3246
|
+
"""Generate progress report."""
|
|
3247
|
+
console = ctx.obj['console']
|
|
3248
|
+
|
|
3249
|
+
try:
|
|
3250
|
+
from claude_dev_cli.logging import MarkdownLogger
|
|
3251
|
+
|
|
3252
|
+
logger = MarkdownLogger()
|
|
3253
|
+
report = logger.get_report()
|
|
3254
|
+
|
|
3255
|
+
md = Markdown(report)
|
|
3256
|
+
console.print(md)
|
|
3257
|
+
|
|
3258
|
+
except Exception as e:
|
|
3259
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
3260
|
+
sys.exit(1)
|
|
3261
|
+
|
|
3262
|
+
|
|
3263
|
+
@main.group()
|
|
3264
|
+
def notify() -> None:
|
|
3265
|
+
"""Test notifications."""
|
|
3266
|
+
pass
|
|
3267
|
+
|
|
3268
|
+
|
|
3269
|
+
@notify.command('test')
|
|
3270
|
+
@click.option('--topic', default='cdc-test', help='Ntfy topic')
|
|
3271
|
+
@click.pass_context
|
|
3272
|
+
def notify_test(ctx: click.Context, topic: str) -> None:
|
|
3273
|
+
"""Send a test notification."""
|
|
3274
|
+
console = ctx.obj['console']
|
|
3275
|
+
|
|
3276
|
+
try:
|
|
3277
|
+
from claude_dev_cli.notifications import NtfyNotifier, NotificationPriority
|
|
3278
|
+
|
|
3279
|
+
notifier = NtfyNotifier(topic=topic)
|
|
3280
|
+
|
|
3281
|
+
success = notifier.send(
|
|
3282
|
+
title="Test Notification",
|
|
3283
|
+
message="✅ claude-dev-cli notifications are working!",
|
|
3284
|
+
priority=NotificationPriority.NORMAL,
|
|
3285
|
+
tags=["white_check_mark", "test"]
|
|
3286
|
+
)
|
|
3287
|
+
|
|
3288
|
+
if success:
|
|
3289
|
+
console.print(f"[green]✅ Notification sent![/green]")
|
|
3290
|
+
console.print(f"\n[dim]Check: https://ntfy.sh/{topic}[/dim]")
|
|
3291
|
+
else:
|
|
3292
|
+
console.print("[red]❌ Failed to send notification[/red]")
|
|
3293
|
+
sys.exit(1)
|
|
3294
|
+
|
|
3295
|
+
except Exception as e:
|
|
3296
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
3297
|
+
sys.exit(1)
|
|
3298
|
+
|
|
3299
|
+
|
|
2876
3300
|
if __name__ == '__main__':
|
|
2877
3301
|
main(obj={})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Abstract progress logger interface."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class LogEntry:
|
|
11
|
+
"""A log entry for progress tracking."""
|
|
12
|
+
timestamp: datetime
|
|
13
|
+
message: str
|
|
14
|
+
ticket_id: Optional[str] = None
|
|
15
|
+
level: str = "info" # info, success, error, warning
|
|
16
|
+
metadata: Dict[str, Any] = None
|
|
17
|
+
|
|
18
|
+
def __post_init__(self):
|
|
19
|
+
"""Initialize metadata."""
|
|
20
|
+
if self.metadata is None:
|
|
21
|
+
self.metadata = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProgressLogger(ABC):
|
|
25
|
+
"""Abstract base class for progress logging."""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def init(self, project_name: str) -> bool:
|
|
29
|
+
"""Initialize logging for a project.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
project_name: Name of the project
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if successful
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def log(
|
|
41
|
+
self,
|
|
42
|
+
message: str,
|
|
43
|
+
ticket_id: Optional[str] = None,
|
|
44
|
+
level: str = "info",
|
|
45
|
+
**metadata
|
|
46
|
+
) -> bool:
|
|
47
|
+
"""Log a progress entry.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
message: Log message
|
|
51
|
+
ticket_id: Related ticket ID
|
|
52
|
+
level: Log level (info, success, error, warning)
|
|
53
|
+
**metadata: Additional metadata
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if successful
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def link_artifact(self, ticket_id: str, artifact_path: str) -> bool:
|
|
62
|
+
"""Link an artifact (file, code, etc.) to a ticket.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
ticket_id: Ticket identifier
|
|
66
|
+
artifact_path: Path to artifact
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if successful
|
|
70
|
+
"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def get_report(self) -> str:
|
|
75
|
+
"""Generate a progress report.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Formatted progress report
|
|
79
|
+
"""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def get_logger_name(self) -> str:
|
|
83
|
+
"""Get logger backend name."""
|
|
84
|
+
return self.__class__.__name__.replace('Logger', '').lower()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Markdown-based progress logger."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from claude_dev_cli.logging.logger import ProgressLogger, LogEntry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MarkdownLogger(ProgressLogger):
|
|
11
|
+
"""Markdown file-based progress logger.
|
|
12
|
+
|
|
13
|
+
Creates a progress.md file to track project execution.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, log_dir: Optional[Path] = None):
|
|
17
|
+
"""Initialize markdown logger.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
log_dir: Directory for log files (default: .cdc-logs)
|
|
21
|
+
"""
|
|
22
|
+
self.log_dir = log_dir or Path.cwd() / ".cdc-logs"
|
|
23
|
+
self.log_file = self.log_dir / "progress.md"
|
|
24
|
+
self.entries: List[LogEntry] = []
|
|
25
|
+
|
|
26
|
+
def init(self, project_name: str) -> bool:
|
|
27
|
+
"""Initialize logging directory and file."""
|
|
28
|
+
try:
|
|
29
|
+
self.log_dir.mkdir(exist_ok=True)
|
|
30
|
+
|
|
31
|
+
# Create initial progress.md with header
|
|
32
|
+
with open(self.log_file, 'w') as f:
|
|
33
|
+
f.write(f"# {project_name} - Progress Log\n\n")
|
|
34
|
+
f.write(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
35
|
+
f.write("---\n\n")
|
|
36
|
+
|
|
37
|
+
return True
|
|
38
|
+
except OSError:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def log(
|
|
42
|
+
self,
|
|
43
|
+
message: str,
|
|
44
|
+
ticket_id: Optional[str] = None,
|
|
45
|
+
level: str = "info",
|
|
46
|
+
**metadata
|
|
47
|
+
) -> bool:
|
|
48
|
+
"""Log an entry to markdown file."""
|
|
49
|
+
try:
|
|
50
|
+
entry = LogEntry(
|
|
51
|
+
timestamp=datetime.now(),
|
|
52
|
+
message=message,
|
|
53
|
+
ticket_id=ticket_id,
|
|
54
|
+
level=level,
|
|
55
|
+
metadata=metadata
|
|
56
|
+
)
|
|
57
|
+
self.entries.append(entry)
|
|
58
|
+
|
|
59
|
+
# Append to file
|
|
60
|
+
with open(self.log_file, 'a') as f:
|
|
61
|
+
# Format entry
|
|
62
|
+
icon = self._get_level_icon(level)
|
|
63
|
+
timestamp_str = entry.timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
|
64
|
+
|
|
65
|
+
f.write(f"## {icon} {timestamp_str}\n\n")
|
|
66
|
+
|
|
67
|
+
if ticket_id:
|
|
68
|
+
f.write(f"**Ticket:** {ticket_id}\n\n")
|
|
69
|
+
|
|
70
|
+
f.write(f"{message}\n\n")
|
|
71
|
+
|
|
72
|
+
# Add metadata if present
|
|
73
|
+
if metadata:
|
|
74
|
+
f.write("**Details:**\n")
|
|
75
|
+
for key, value in metadata.items():
|
|
76
|
+
f.write(f"- {key}: {value}\n")
|
|
77
|
+
f.write("\n")
|
|
78
|
+
|
|
79
|
+
f.write("---\n\n")
|
|
80
|
+
|
|
81
|
+
return True
|
|
82
|
+
except OSError:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
def link_artifact(self, ticket_id: str, artifact_path: str) -> bool:
|
|
86
|
+
"""Link an artifact to a ticket in the log."""
|
|
87
|
+
return self.log(
|
|
88
|
+
f"📎 Generated artifact: `{artifact_path}`",
|
|
89
|
+
ticket_id=ticket_id,
|
|
90
|
+
level="info",
|
|
91
|
+
artifact=artifact_path
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def get_report(self) -> str:
|
|
95
|
+
"""Generate a summary report."""
|
|
96
|
+
if not self.log_file.exists():
|
|
97
|
+
return "No progress log found."
|
|
98
|
+
|
|
99
|
+
with open(self.log_file, 'r') as f:
|
|
100
|
+
content = f.read()
|
|
101
|
+
|
|
102
|
+
# Add summary at top
|
|
103
|
+
total_entries = len(self.entries)
|
|
104
|
+
success_count = sum(1 for e in self.entries if e.level == "success")
|
|
105
|
+
error_count = sum(1 for e in self.entries if e.level == "error")
|
|
106
|
+
|
|
107
|
+
summary = f"""# Progress Summary
|
|
108
|
+
|
|
109
|
+
**Total Entries:** {total_entries}
|
|
110
|
+
**Successes:** ✅ {success_count}
|
|
111
|
+
**Errors:** ❌ {error_count}
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
{content}
|
|
116
|
+
"""
|
|
117
|
+
return summary
|
|
118
|
+
|
|
119
|
+
def _get_level_icon(self, level: str) -> str:
|
|
120
|
+
"""Get emoji icon for log level."""
|
|
121
|
+
icons = {
|
|
122
|
+
"info": "ℹ️",
|
|
123
|
+
"success": "✅",
|
|
124
|
+
"error": "❌",
|
|
125
|
+
"warning": "⚠️"
|
|
126
|
+
}
|
|
127
|
+
return icons.get(level, "📝")
|
|
128
|
+
|
|
129
|
+
def get_logger_name(self) -> str:
|
|
130
|
+
"""Return logger name."""
|
|
131
|
+
return "markdown"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Notification system for claude-dev-cli project management."""
|
|
2
|
+
|
|
3
|
+
from claude_dev_cli.notifications.notifier import Notifier, NotificationPriority
|
|
4
|
+
from claude_dev_cli.notifications.ntfy import NtfyNotifier
|
|
5
|
+
|
|
6
|
+
__all__ = ["Notifier", "NotificationPriority", "NtfyNotifier"]
|