mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__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 mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""CLI commands for project updates (Linear project status updates)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ..core.models import ProjectUpdateHealth
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(name="project-update", help="Project status update management")
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _format_health_indicator(health: ProjectUpdateHealth | str | None) -> str:
|
|
18
|
+
"""Format health status with color-coded indicators.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
health: Health status enum or string value
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Formatted health indicator with color and icon
|
|
25
|
+
"""
|
|
26
|
+
if not health:
|
|
27
|
+
return "[dim]○ Not Set[/dim]"
|
|
28
|
+
|
|
29
|
+
# Handle both enum and string values
|
|
30
|
+
health_str = health.value if isinstance(health, ProjectUpdateHealth) else health
|
|
31
|
+
|
|
32
|
+
health_indicators = {
|
|
33
|
+
"on_track": "[green]✓ On Track[/green]",
|
|
34
|
+
"at_risk": "[yellow]⚠ At Risk[/yellow]",
|
|
35
|
+
"off_track": "[red]✗ Off Track[/red]",
|
|
36
|
+
"complete": "[blue]✓ Complete[/blue]",
|
|
37
|
+
"inactive": "[dim]○ Inactive[/dim]",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return health_indicators.get(health_str, f"[dim]{health_str}[/dim]")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _format_relative_time(dt: datetime) -> str:
|
|
44
|
+
"""Format datetime as relative time string.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
dt: Datetime to format
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Human-readable relative time (e.g., "2 hours ago")
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
from humanize import naturaltime
|
|
54
|
+
|
|
55
|
+
return naturaltime(dt)
|
|
56
|
+
except ImportError:
|
|
57
|
+
# Fallback to ISO format if humanize not available
|
|
58
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def _create_update_async(
|
|
62
|
+
project_id: str,
|
|
63
|
+
body: str,
|
|
64
|
+
health: ProjectUpdateHealth | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Async implementation of create command."""
|
|
67
|
+
from .main import get_adapter
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
adapter = get_adapter()
|
|
71
|
+
|
|
72
|
+
# Check if adapter supports project updates
|
|
73
|
+
if not hasattr(adapter, "create_project_update"):
|
|
74
|
+
adapter_name = getattr(adapter, "adapter_name", type(adapter).__name__)
|
|
75
|
+
console.print(
|
|
76
|
+
f"[red]✗[/red] Adapter '{adapter_name}' does not support project updates"
|
|
77
|
+
)
|
|
78
|
+
console.print(
|
|
79
|
+
"[dim]Project updates are supported by: Linear, GitHub V2, Asana[/dim]"
|
|
80
|
+
)
|
|
81
|
+
raise typer.Exit(1) from None
|
|
82
|
+
|
|
83
|
+
# Validate body is not empty
|
|
84
|
+
if not body or not body.strip():
|
|
85
|
+
console.print("[red]✗[/red] Update body cannot be empty")
|
|
86
|
+
raise typer.Exit(1) from None
|
|
87
|
+
|
|
88
|
+
# Create the project update
|
|
89
|
+
console.print(f"[dim]Creating project update for '{project_id}'...[/dim]")
|
|
90
|
+
update = await adapter.create_project_update(
|
|
91
|
+
project_id=project_id,
|
|
92
|
+
body=body,
|
|
93
|
+
health=health,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Display success message with update details
|
|
97
|
+
console.print("\n[green]✓[/green] Project update created successfully!")
|
|
98
|
+
console.print(f" Update ID: [cyan]{update.id}[/cyan]")
|
|
99
|
+
console.print(
|
|
100
|
+
f" Project: [bold]{update.project_name or update.project_id}[/bold]"
|
|
101
|
+
)
|
|
102
|
+
console.print(f" Health: {_format_health_indicator(update.health)}")
|
|
103
|
+
console.print(f" Created: {update.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
104
|
+
|
|
105
|
+
if update.url:
|
|
106
|
+
console.print(f" URL: [link]{update.url}[/link]")
|
|
107
|
+
|
|
108
|
+
# Show preview of body
|
|
109
|
+
preview = body[:100] + "..." if len(body) > 100 else body
|
|
110
|
+
console.print(f" Body Preview: {preview}")
|
|
111
|
+
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
console.print(f"[red]✗[/red] {str(e)}")
|
|
114
|
+
raise typer.Exit(1) from e
|
|
115
|
+
except Exception as e:
|
|
116
|
+
console.print(f"[red]✗[/red] Unexpected error: {str(e)}")
|
|
117
|
+
raise typer.Exit(1) from e
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def _list_updates_async(
|
|
121
|
+
project_id: str,
|
|
122
|
+
limit: int = 10,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Async implementation of list command."""
|
|
125
|
+
from .main import get_adapter
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
adapter = get_adapter()
|
|
129
|
+
|
|
130
|
+
# Check if adapter supports project updates
|
|
131
|
+
if not hasattr(adapter, "list_project_updates"):
|
|
132
|
+
adapter_name = getattr(adapter, "adapter_name", type(adapter).__name__)
|
|
133
|
+
console.print(
|
|
134
|
+
f"[red]✗[/red] Adapter '{adapter_name}' does not support project updates"
|
|
135
|
+
)
|
|
136
|
+
raise typer.Exit(1) from None
|
|
137
|
+
|
|
138
|
+
# List project updates
|
|
139
|
+
console.print(f"[dim]Fetching updates for project '{project_id}'...[/dim]")
|
|
140
|
+
updates = await adapter.list_project_updates(
|
|
141
|
+
project_id=project_id,
|
|
142
|
+
limit=limit,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not updates:
|
|
146
|
+
console.print(
|
|
147
|
+
f"\n[yellow]No updates found for project '{project_id}'[/yellow]"
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# Create Rich table
|
|
152
|
+
table = Table(title=f"Project Updates ({len(updates)} total)")
|
|
153
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
154
|
+
table.add_column("Date", style="dim")
|
|
155
|
+
table.add_column("Health", no_wrap=True)
|
|
156
|
+
table.add_column("Author", style="blue")
|
|
157
|
+
table.add_column("Preview", style="white")
|
|
158
|
+
|
|
159
|
+
for update in updates:
|
|
160
|
+
# Format date
|
|
161
|
+
date_str = (
|
|
162
|
+
_format_relative_time(update.created_at)
|
|
163
|
+
if update.created_at
|
|
164
|
+
else "Unknown"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Format health
|
|
168
|
+
health_str = _format_health_indicator(update.health)
|
|
169
|
+
|
|
170
|
+
# Format author
|
|
171
|
+
author = update.author_name or update.author_id or "Unknown"
|
|
172
|
+
|
|
173
|
+
# Preview (first 50 chars of body)
|
|
174
|
+
preview = update.body[:50] + "..." if len(update.body) > 50 else update.body
|
|
175
|
+
|
|
176
|
+
table.add_row(
|
|
177
|
+
update.id[:8] + "..." if len(update.id) > 8 else update.id,
|
|
178
|
+
date_str,
|
|
179
|
+
health_str,
|
|
180
|
+
author,
|
|
181
|
+
preview,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
console.print(table)
|
|
185
|
+
|
|
186
|
+
except ValueError as e:
|
|
187
|
+
console.print(f"[red]✗[/red] {str(e)}")
|
|
188
|
+
raise typer.Exit(1) from e
|
|
189
|
+
except Exception as e:
|
|
190
|
+
console.print(f"[red]✗[/red] Unexpected error: {str(e)}")
|
|
191
|
+
raise typer.Exit(1) from e
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def _get_update_async(update_id: str) -> None:
|
|
195
|
+
"""Async implementation of get command."""
|
|
196
|
+
from .main import get_adapter
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
adapter = get_adapter()
|
|
200
|
+
|
|
201
|
+
# Check if adapter supports project updates
|
|
202
|
+
if not hasattr(adapter, "get_project_update"):
|
|
203
|
+
adapter_name = getattr(adapter, "adapter_name", type(adapter).__name__)
|
|
204
|
+
console.print(
|
|
205
|
+
f"[red]✗[/red] Adapter '{adapter_name}' does not support project updates"
|
|
206
|
+
)
|
|
207
|
+
raise typer.Exit(1) from None
|
|
208
|
+
|
|
209
|
+
# Get project update
|
|
210
|
+
console.print(f"[dim]Fetching update '{update_id}'...[/dim]")
|
|
211
|
+
update = await adapter.get_project_update(update_id=update_id)
|
|
212
|
+
|
|
213
|
+
# Build detailed display content
|
|
214
|
+
content_lines = [
|
|
215
|
+
f"[bold]Update ID:[/bold] {update.id}",
|
|
216
|
+
f"[bold]Project:[/bold] {update.project_name or update.project_id}",
|
|
217
|
+
f"[bold]Health:[/bold] {_format_health_indicator(update.health)}",
|
|
218
|
+
f"[bold]Created:[/bold] {update.created_at.strftime('%Y-%m-%d %H:%M:%S')}",
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
if update.author_name or update.author_id:
|
|
222
|
+
author = update.author_name or update.author_id
|
|
223
|
+
content_lines.append(f"[bold]Author:[/bold] {author}")
|
|
224
|
+
|
|
225
|
+
if update.url:
|
|
226
|
+
content_lines.append(f"[bold]URL:[/bold] [link]{update.url}[/link]")
|
|
227
|
+
|
|
228
|
+
# Add body
|
|
229
|
+
content_lines.append("")
|
|
230
|
+
content_lines.append("[bold]Body:[/bold]")
|
|
231
|
+
content_lines.append(update.body)
|
|
232
|
+
|
|
233
|
+
# Add Linear-specific fields if present
|
|
234
|
+
if update.diff_markdown:
|
|
235
|
+
content_lines.append("")
|
|
236
|
+
content_lines.append("[bold]Diff (Changes Since Last Update):[/bold]")
|
|
237
|
+
content_lines.append(update.diff_markdown)
|
|
238
|
+
|
|
239
|
+
if update.is_stale is not None:
|
|
240
|
+
stale_indicator = (
|
|
241
|
+
"[red]⚠ Stale[/red]" if update.is_stale else "[green]✓ Current[/green]"
|
|
242
|
+
)
|
|
243
|
+
content_lines.append("")
|
|
244
|
+
content_lines.append(f"[bold]Freshness:[/bold] {stale_indicator}")
|
|
245
|
+
|
|
246
|
+
# Display as Rich panel
|
|
247
|
+
panel = Panel(
|
|
248
|
+
"\n".join(content_lines),
|
|
249
|
+
title="[bold]Project Update Details[/bold]",
|
|
250
|
+
border_style="cyan",
|
|
251
|
+
expand=False,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
console.print(panel)
|
|
255
|
+
|
|
256
|
+
except ValueError as e:
|
|
257
|
+
console.print(f"[red]✗[/red] {str(e)}")
|
|
258
|
+
raise typer.Exit(1) from e
|
|
259
|
+
except Exception as e:
|
|
260
|
+
console.print(f"[red]✗[/red] Unexpected error: {str(e)}")
|
|
261
|
+
raise typer.Exit(1) from e
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@app.command("create")
|
|
265
|
+
def create_update(
|
|
266
|
+
project_id: str = typer.Argument(
|
|
267
|
+
...,
|
|
268
|
+
help="Project identifier (UUID, slugId, short ID, or URL)",
|
|
269
|
+
),
|
|
270
|
+
body: str = typer.Argument(
|
|
271
|
+
...,
|
|
272
|
+
help="Update content (markdown formatted)",
|
|
273
|
+
),
|
|
274
|
+
health: ProjectUpdateHealth | None = typer.Option(
|
|
275
|
+
None,
|
|
276
|
+
"--health",
|
|
277
|
+
"-h",
|
|
278
|
+
help="Project health status (on_track, at_risk, off_track, complete, inactive)",
|
|
279
|
+
),
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Create a new project status update.
|
|
282
|
+
|
|
283
|
+
Creates a status update for a project with optional health indicator.
|
|
284
|
+
Linear projects will automatically generate a diff showing changes.
|
|
285
|
+
|
|
286
|
+
Examples:
|
|
287
|
+
# Create update with health status
|
|
288
|
+
mcp-ticket project-update create "PROJ-123" "Sprint completed successfully" --health on_track
|
|
289
|
+
|
|
290
|
+
# Create update using project URL
|
|
291
|
+
mcp-ticket project-update create "https://linear.app/team/project/proj-slug" "Weekly update"
|
|
292
|
+
|
|
293
|
+
# Create update without health indicator
|
|
294
|
+
mcp-ticket project-update create "mcp-ticketer-eac28953c267" "Development ongoing"
|
|
295
|
+
"""
|
|
296
|
+
asyncio.run(_create_update_async(project_id, body, health))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@app.command("list")
|
|
300
|
+
def list_updates(
|
|
301
|
+
project_id: str = typer.Argument(
|
|
302
|
+
...,
|
|
303
|
+
help="Project identifier (UUID, slugId, short ID, or URL)",
|
|
304
|
+
),
|
|
305
|
+
limit: int = typer.Option(
|
|
306
|
+
10,
|
|
307
|
+
"--limit",
|
|
308
|
+
"-l",
|
|
309
|
+
help="Maximum number of updates to return",
|
|
310
|
+
min=1,
|
|
311
|
+
max=100,
|
|
312
|
+
),
|
|
313
|
+
) -> None:
|
|
314
|
+
"""List recent project updates.
|
|
315
|
+
|
|
316
|
+
Retrieves recent status updates for a project, ordered by creation date (newest first).
|
|
317
|
+
|
|
318
|
+
Examples:
|
|
319
|
+
# List last 10 updates (default)
|
|
320
|
+
mcp-ticket project-update list "PROJ-123"
|
|
321
|
+
|
|
322
|
+
# List last 20 updates
|
|
323
|
+
mcp-ticket project-update list "PROJ-123" --limit 20
|
|
324
|
+
|
|
325
|
+
# List updates using project URL
|
|
326
|
+
mcp-ticket project-update list "https://linear.app/team/project/proj-slug"
|
|
327
|
+
"""
|
|
328
|
+
asyncio.run(_list_updates_async(project_id, limit))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.command("get")
|
|
332
|
+
def get_update(
|
|
333
|
+
update_id: str = typer.Argument(
|
|
334
|
+
...,
|
|
335
|
+
help="Project update identifier (UUID)",
|
|
336
|
+
),
|
|
337
|
+
) -> None:
|
|
338
|
+
"""Get detailed information about a specific project update.
|
|
339
|
+
|
|
340
|
+
Retrieves full details including body, health status, author, and Linear-specific
|
|
341
|
+
fields like auto-generated diffs and staleness indicators.
|
|
342
|
+
|
|
343
|
+
Examples:
|
|
344
|
+
# Get update details
|
|
345
|
+
mcp-ticket project-update get "update-uuid-here"
|
|
346
|
+
|
|
347
|
+
# View update with full diff
|
|
348
|
+
mcp-ticket project-update get "abc123def456"
|
|
349
|
+
"""
|
|
350
|
+
asyncio.run(_get_update_async(update_id))
|