mcp-ticketer 0.4.11__py3-none-any.whl → 0.12.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 mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +9 -3
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +313 -96
- mcp_ticketer/adapters/jira.py +251 -1
- mcp_ticketer/adapters/linear/adapter.py +524 -22
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +1 -1
- mcp_ticketer/cli/codex_configure.py +80 -1
- mcp_ticketer/cli/configure.py +33 -43
- mcp_ticketer/cli/diagnostics.py +18 -16
- mcp_ticketer/cli/discover.py +288 -21
- mcp_ticketer/cli/gemini_configure.py +1 -1
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +1199 -227
- mcp_ticketer/cli/mcp_configure.py +1 -1
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +14 -13
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +12 -0
- mcp_ticketer/core/adapter.py +4 -4
- mcp_ticketer/core/config.py +17 -10
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +1 -1
- mcp_ticketer/core/models.py +1 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +17 -1
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/main.py +82 -69
- mcp_ticketer/mcp/server/tools/__init__.py +9 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +14 -12
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -69,7 +69,7 @@ class LinearGraphQLClient:
|
|
|
69
69
|
return client
|
|
70
70
|
|
|
71
71
|
except Exception as e:
|
|
72
|
-
raise AdapterError(f"Failed to create Linear client: {e}", "linear")
|
|
72
|
+
raise AdapterError(f"Failed to create Linear client: {e}", "linear") from e
|
|
73
73
|
|
|
74
74
|
async def execute_query(
|
|
75
75
|
self,
|
|
@@ -110,15 +110,19 @@ class LinearGraphQLClient:
|
|
|
110
110
|
status_code = e.response.status
|
|
111
111
|
|
|
112
112
|
if status_code == 401:
|
|
113
|
-
raise AuthenticationError(
|
|
113
|
+
raise AuthenticationError(
|
|
114
|
+
"Invalid Linear API key", "linear"
|
|
115
|
+
) from e
|
|
114
116
|
elif status_code == 403:
|
|
115
|
-
raise AuthenticationError(
|
|
117
|
+
raise AuthenticationError(
|
|
118
|
+
"Insufficient permissions", "linear"
|
|
119
|
+
) from e
|
|
116
120
|
elif status_code == 429:
|
|
117
121
|
# Rate limit exceeded
|
|
118
122
|
retry_after = e.response.headers.get("Retry-After", "60")
|
|
119
123
|
raise RateLimitError(
|
|
120
124
|
"Linear API rate limit exceeded", "linear", retry_after
|
|
121
|
-
)
|
|
125
|
+
) from e
|
|
122
126
|
elif status_code >= 500:
|
|
123
127
|
# Server error - retry
|
|
124
128
|
if attempt < retries:
|
|
@@ -126,13 +130,13 @@ class LinearGraphQLClient:
|
|
|
126
130
|
continue
|
|
127
131
|
raise AdapterError(
|
|
128
132
|
f"Linear API server error: {status_code}", "linear"
|
|
129
|
-
)
|
|
133
|
+
) from e
|
|
130
134
|
|
|
131
135
|
# Network or other transport error
|
|
132
136
|
if attempt < retries:
|
|
133
137
|
await asyncio.sleep(2**attempt)
|
|
134
138
|
continue
|
|
135
|
-
raise AdapterError(f"Linear API transport error: {e}", "linear")
|
|
139
|
+
raise AdapterError(f"Linear API transport error: {e}", "linear") from e
|
|
136
140
|
|
|
137
141
|
except Exception as e:
|
|
138
142
|
# GraphQL or other errors
|
|
@@ -145,15 +149,19 @@ class LinearGraphQLClient:
|
|
|
145
149
|
):
|
|
146
150
|
raise AuthenticationError(
|
|
147
151
|
f"Linear authentication failed: {error_msg}", "linear"
|
|
148
|
-
)
|
|
152
|
+
) from e
|
|
149
153
|
elif "rate limit" in error_msg.lower():
|
|
150
|
-
raise RateLimitError(
|
|
154
|
+
raise RateLimitError(
|
|
155
|
+
"Linear API rate limit exceeded", "linear"
|
|
156
|
+
) from e
|
|
151
157
|
|
|
152
158
|
# Generic error
|
|
153
159
|
if attempt < retries:
|
|
154
160
|
await asyncio.sleep(2**attempt)
|
|
155
161
|
continue
|
|
156
|
-
raise AdapterError(
|
|
162
|
+
raise AdapterError(
|
|
163
|
+
f"Linear GraphQL error: {error_msg}", "linear"
|
|
164
|
+
) from e
|
|
157
165
|
|
|
158
166
|
# Should never reach here
|
|
159
167
|
raise AdapterError("Maximum retries exceeded", "linear")
|
|
@@ -266,6 +274,50 @@ class LinearGraphQLClient:
|
|
|
266
274
|
except Exception:
|
|
267
275
|
return None
|
|
268
276
|
|
|
277
|
+
async def get_users_by_name(self, name: str) -> list[dict[str, Any]]:
|
|
278
|
+
"""Search users by display name or full name.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
name: Display name or full name to search for
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
List of matching users (may be empty)
|
|
285
|
+
|
|
286
|
+
"""
|
|
287
|
+
import logging
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
query = """
|
|
291
|
+
query SearchUsers($nameFilter: String!) {
|
|
292
|
+
users(
|
|
293
|
+
filter: {
|
|
294
|
+
or: [
|
|
295
|
+
{ displayName: { containsIgnoreCase: $nameFilter } }
|
|
296
|
+
{ name: { containsIgnoreCase: $nameFilter } }
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
first: 10
|
|
300
|
+
) {
|
|
301
|
+
nodes {
|
|
302
|
+
id
|
|
303
|
+
name
|
|
304
|
+
email
|
|
305
|
+
displayName
|
|
306
|
+
avatarUrl
|
|
307
|
+
active
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
result = await self.execute_query(query, {"nameFilter": name})
|
|
314
|
+
users = result.get("users", {}).get("nodes", [])
|
|
315
|
+
return [u for u in users if u.get("active", True)] # Filter active users
|
|
316
|
+
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logging.getLogger(__name__).warning(f"Failed to search users by name: {e}")
|
|
319
|
+
return []
|
|
320
|
+
|
|
269
321
|
async def close(self) -> None:
|
|
270
322
|
"""Close the client connection.
|
|
271
323
|
|
|
@@ -10,7 +10,10 @@ from .types import extract_linear_metadata, get_universal_priority, get_universa
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
|
|
13
|
-
"""Convert Linear issue data to universal Task model.
|
|
13
|
+
"""Convert Linear issue or sub-issue data to universal Task model.
|
|
14
|
+
|
|
15
|
+
Handles both top-level issues (no parent) and sub-issues (child items
|
|
16
|
+
with a parent issue).
|
|
14
17
|
|
|
15
18
|
Args:
|
|
16
19
|
issue_data: Raw Linear issue data from GraphQL
|
|
@@ -203,7 +206,10 @@ def map_linear_comment_to_comment(
|
|
|
203
206
|
|
|
204
207
|
|
|
205
208
|
def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
|
|
206
|
-
"""Build Linear issue input from universal Task model.
|
|
209
|
+
"""Build Linear issue or sub-issue input from universal Task model.
|
|
210
|
+
|
|
211
|
+
Creates input for a top-level issue when task.parent_issue is not set,
|
|
212
|
+
or for a sub-issue when task.parent_issue is provided.
|
|
207
213
|
|
|
208
214
|
Args:
|
|
209
215
|
task: Universal Task model
|
|
@@ -215,7 +221,7 @@ def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
|
|
|
215
221
|
"""
|
|
216
222
|
from .types import get_linear_priority
|
|
217
223
|
|
|
218
|
-
issue_input = {
|
|
224
|
+
issue_input: dict[str, Any] = {
|
|
219
225
|
"title": task.title,
|
|
220
226
|
"teamId": team_id,
|
|
221
227
|
}
|
mcp_ticketer/cache/memory.py
CHANGED
|
@@ -115,7 +115,7 @@ class MemoryCache:
|
|
|
115
115
|
return len(self._cache)
|
|
116
116
|
|
|
117
117
|
@staticmethod
|
|
118
|
-
def generate_key(*args, **kwargs) -> str:
|
|
118
|
+
def generate_key(*args: Any, **kwargs: Any) -> str:
|
|
119
119
|
"""Generate cache key from arguments.
|
|
120
120
|
|
|
121
121
|
Args:
|
|
@@ -139,7 +139,7 @@ def cache_decorator(
|
|
|
139
139
|
key_prefix: str = "",
|
|
140
140
|
cache_instance: MemoryCache | None = None,
|
|
141
141
|
) -> Callable:
|
|
142
|
-
"""
|
|
142
|
+
"""Decorate async function to cache its results.
|
|
143
143
|
|
|
144
144
|
Args:
|
|
145
145
|
ttl: TTL for cached results
|
|
@@ -155,7 +155,7 @@ def cache_decorator(
|
|
|
155
155
|
|
|
156
156
|
def decorator(func: Callable) -> Callable:
|
|
157
157
|
@wraps(func)
|
|
158
|
-
async def wrapper(*args, **kwargs):
|
|
158
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
159
159
|
# Generate cache key
|
|
160
160
|
base_key = MemoryCache.generate_key(*args, **kwargs)
|
|
161
161
|
cache_key = f"{key_prefix}:{func.__name__}:{base_key}"
|
|
@@ -343,7 +343,7 @@ def _provide_recommendations(console: Console) -> None:
|
|
|
343
343
|
console.print("• For local files: [cyan]mcp-ticketer init aitrackdown[/cyan]")
|
|
344
344
|
|
|
345
345
|
console.print("\n[bold]Test Configuration:[/bold]")
|
|
346
|
-
console.print("• Run diagnostics: [cyan]mcp-ticketer
|
|
346
|
+
console.print("• Run diagnostics: [cyan]mcp-ticketer doctor[/cyan]")
|
|
347
347
|
console.print(
|
|
348
348
|
"• Test ticket creation: [cyan]mcp-ticketer create 'Test ticket'[/cyan]"
|
|
349
349
|
)
|
|
@@ -243,7 +243,7 @@ def configure_auggie_mcp(force: bool = False) -> None:
|
|
|
243
243
|
"Could not find mcp-ticketer Python executable. "
|
|
244
244
|
"Please ensure mcp-ticketer is installed.\n"
|
|
245
245
|
"Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
|
|
246
|
-
)
|
|
246
|
+
) from e
|
|
247
247
|
|
|
248
248
|
# Step 2: Load project configuration
|
|
249
249
|
console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
|
|
@@ -153,6 +153,75 @@ def create_codex_server_config(
|
|
|
153
153
|
return config
|
|
154
154
|
|
|
155
155
|
|
|
156
|
+
def _test_configuration(adapter: str, project_config: dict) -> bool:
|
|
157
|
+
"""Test the configuration by validating adapter credentials.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
adapter: Adapter type (linear, github, jira, aitrackdown)
|
|
161
|
+
project_config: Project configuration dict
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if validation passed, False otherwise
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
from ..core import AdapterRegistry
|
|
169
|
+
|
|
170
|
+
# Get adapter configuration
|
|
171
|
+
adapters_config = project_config.get("adapters", {})
|
|
172
|
+
adapter_config = adapters_config.get(adapter, {})
|
|
173
|
+
|
|
174
|
+
# Test adapter instantiation
|
|
175
|
+
console.print(f" Testing {adapter} adapter...")
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
adapter_instance = AdapterRegistry.get_adapter(adapter, adapter_config)
|
|
179
|
+
console.print(" [green]✓[/green] Adapter instantiated successfully")
|
|
180
|
+
|
|
181
|
+
# Test credentials if validation method exists
|
|
182
|
+
if hasattr(adapter_instance, "validate_credentials"):
|
|
183
|
+
console.print(f" Validating {adapter} credentials...")
|
|
184
|
+
is_valid, error_msg = adapter_instance.validate_credentials()
|
|
185
|
+
|
|
186
|
+
if is_valid:
|
|
187
|
+
console.print(" [green]✓[/green] Credentials are valid")
|
|
188
|
+
return True
|
|
189
|
+
else:
|
|
190
|
+
console.print(
|
|
191
|
+
f" [red]✗[/red] Credential validation failed: {error_msg}"
|
|
192
|
+
)
|
|
193
|
+
return False
|
|
194
|
+
else:
|
|
195
|
+
# No validation method, assume valid
|
|
196
|
+
console.print(
|
|
197
|
+
f" [yellow]○[/yellow] No credential validation available for {adapter}"
|
|
198
|
+
)
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
console.print(f" [red]✗[/red] Adapter instantiation failed: {e}")
|
|
203
|
+
|
|
204
|
+
# Provide helpful error messages based on adapter type
|
|
205
|
+
if adapter == "linear":
|
|
206
|
+
console.print(
|
|
207
|
+
"\n [yellow]Linear requires:[/yellow] LINEAR_API_KEY and LINEAR_TEAM_ID"
|
|
208
|
+
)
|
|
209
|
+
elif adapter == "github":
|
|
210
|
+
console.print(
|
|
211
|
+
"\n [yellow]GitHub requires:[/yellow] GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO"
|
|
212
|
+
)
|
|
213
|
+
elif adapter == "jira":
|
|
214
|
+
console.print(
|
|
215
|
+
"\n [yellow]JIRA requires:[/yellow] JIRA_SERVER, JIRA_EMAIL, JIRA_API_TOKEN"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
console.print(f" [red]✗[/red] Configuration test error: {e}")
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
|
|
156
225
|
def remove_codex_mcp(dry_run: bool = False) -> None:
|
|
157
226
|
"""Remove mcp-ticketer from Codex CLI configuration.
|
|
158
227
|
|
|
@@ -248,7 +317,7 @@ def configure_codex_mcp(force: bool = False) -> None:
|
|
|
248
317
|
"Could not find mcp-ticketer Python executable. "
|
|
249
318
|
"Please ensure mcp-ticketer is installed.\n"
|
|
250
319
|
"Install with: pip install mcp-ticketer or pipx install mcp-ticketer"
|
|
251
|
-
)
|
|
320
|
+
) from e
|
|
252
321
|
|
|
253
322
|
# Step 2: Load project configuration
|
|
254
323
|
console.print("\n[cyan]📖 Reading project configuration...[/cyan]")
|
|
@@ -317,6 +386,16 @@ def configure_codex_mcp(force: bool = False) -> None:
|
|
|
317
386
|
f" Environment variables: {list(server_config['env'].keys())}"
|
|
318
387
|
)
|
|
319
388
|
|
|
389
|
+
# Step 9: Test configuration
|
|
390
|
+
console.print("\n[cyan]🧪 Testing configuration...[/cyan]")
|
|
391
|
+
test_success = _test_configuration(adapter, project_config)
|
|
392
|
+
|
|
393
|
+
if not test_success:
|
|
394
|
+
console.print(
|
|
395
|
+
"[yellow]⚠ Configuration saved but validation failed. "
|
|
396
|
+
"Please check your credentials and settings.[/yellow]"
|
|
397
|
+
)
|
|
398
|
+
|
|
320
399
|
# Next steps
|
|
321
400
|
console.print("\n[bold cyan]Next Steps:[/bold cyan]")
|
|
322
401
|
console.print("1. [bold]Restart Codex CLI[/bold] (required for changes)")
|
mcp_ticketer/cli/configure.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Interactive configuration wizard for MCP Ticketer."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
import typer
|
|
6
7
|
from rich.console import Console
|
|
@@ -45,24 +46,25 @@ def configure_wizard() -> None:
|
|
|
45
46
|
|
|
46
47
|
# Step 2: Choose where to save
|
|
47
48
|
console.print("\n[bold]Step 2: Configuration Scope[/bold]")
|
|
48
|
-
console.print(
|
|
49
|
-
|
|
49
|
+
console.print(
|
|
50
|
+
"1. Project-specific (recommended): .mcp-ticketer/config.json in project root"
|
|
51
|
+
)
|
|
52
|
+
console.print("2. Legacy global (deprecated): saves to project config for security")
|
|
50
53
|
|
|
51
|
-
scope = Prompt.ask("Save configuration as", choices=["1", "2"], default="
|
|
54
|
+
scope = Prompt.ask("Save configuration as", choices=["1", "2"], default="1")
|
|
52
55
|
|
|
53
56
|
resolver = ConfigResolver()
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
# Always save to project config (global config removed for security)
|
|
59
|
+
resolver.save_project_config(config)
|
|
60
|
+
config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
61
|
+
|
|
62
|
+
if scope == "2":
|
|
58
63
|
console.print(
|
|
59
|
-
|
|
64
|
+
"[yellow]Note: Global config is deprecated for security. Saving to project config instead.[/yellow]"
|
|
60
65
|
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
resolver.save_project_config(config)
|
|
64
|
-
config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
65
|
-
console.print(f"\n[green]✓[/green] Configuration saved to {config_path}")
|
|
66
|
+
|
|
67
|
+
console.print(f"\n[green]✓[/green] Configuration saved to {config_path}")
|
|
66
68
|
|
|
67
69
|
# Show usage instructions
|
|
68
70
|
console.print("\n[bold]Usage:[/bold]")
|
|
@@ -153,7 +155,7 @@ def _configure_linear() -> AdapterConfig:
|
|
|
153
155
|
is_valid, error = ConfigValidator.validate_linear_config(config_dict)
|
|
154
156
|
if not is_valid:
|
|
155
157
|
console.print(f"[red]Configuration error: {error}[/red]")
|
|
156
|
-
raise typer.Exit(1)
|
|
158
|
+
raise typer.Exit(1) from None
|
|
157
159
|
|
|
158
160
|
return AdapterConfig.from_dict(config_dict)
|
|
159
161
|
|
|
@@ -197,7 +199,7 @@ def _configure_jira() -> AdapterConfig:
|
|
|
197
199
|
is_valid, error = ConfigValidator.validate_jira_config(config_dict)
|
|
198
200
|
if not is_valid:
|
|
199
201
|
console.print(f"[red]Configuration error: {error}[/red]")
|
|
200
|
-
raise typer.Exit(1)
|
|
202
|
+
raise typer.Exit(1) from None
|
|
201
203
|
|
|
202
204
|
return AdapterConfig.from_dict(config_dict)
|
|
203
205
|
|
|
@@ -245,7 +247,7 @@ def _configure_github() -> AdapterConfig:
|
|
|
245
247
|
is_valid, error = ConfigValidator.validate_github_config(config_dict)
|
|
246
248
|
if not is_valid:
|
|
247
249
|
console.print(f"[red]Configuration error: {error}[/red]")
|
|
248
|
-
raise typer.Exit(1)
|
|
250
|
+
raise typer.Exit(1) from None
|
|
249
251
|
|
|
250
252
|
return AdapterConfig.from_dict(config_dict)
|
|
251
253
|
|
|
@@ -295,7 +297,7 @@ def _configure_hybrid_mode() -> TicketerConfig:
|
|
|
295
297
|
|
|
296
298
|
if len(selected_adapters) < 2:
|
|
297
299
|
console.print("[red]Hybrid mode requires at least 2 adapters[/red]")
|
|
298
|
-
raise typer.Exit(1)
|
|
300
|
+
raise typer.Exit(1) from None
|
|
299
301
|
|
|
300
302
|
# Configure each adapter
|
|
301
303
|
adapters = {}
|
|
@@ -365,31 +367,17 @@ def show_current_config() -> None:
|
|
|
365
367
|
resolver = ConfigResolver()
|
|
366
368
|
|
|
367
369
|
# Try to load configs
|
|
368
|
-
global_config = resolver.load_global_config()
|
|
369
370
|
project_config = resolver.load_project_config()
|
|
370
371
|
|
|
371
372
|
console.print("[bold]Current Configuration:[/bold]\n")
|
|
372
373
|
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if global_config.adapters:
|
|
379
|
-
table = Table(title="Global Adapters")
|
|
380
|
-
table.add_column("Adapter", style="cyan")
|
|
381
|
-
table.add_column("Configured", style="green")
|
|
382
|
-
|
|
383
|
-
for name, config in global_config.adapters.items():
|
|
384
|
-
configured = "✓" if config.enabled else "✗"
|
|
385
|
-
table.add_row(name, configured)
|
|
386
|
-
|
|
387
|
-
console.print(table)
|
|
388
|
-
else:
|
|
389
|
-
console.print("[yellow]No global configuration found[/yellow]")
|
|
374
|
+
# Note about global config deprecation
|
|
375
|
+
console.print(
|
|
376
|
+
"[dim]Note: Global config has been deprecated for security reasons.[/dim]"
|
|
377
|
+
)
|
|
378
|
+
console.print("[dim]All configuration is now project-specific only.[/dim]\n")
|
|
390
379
|
|
|
391
380
|
# Project config
|
|
392
|
-
console.print()
|
|
393
381
|
project_config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
394
382
|
if project_config_path.exists():
|
|
395
383
|
console.print(f"[cyan]Project:[/cyan] {project_config_path}")
|
|
@@ -444,7 +432,7 @@ def set_adapter_config(
|
|
|
444
432
|
project_id: str | None = None,
|
|
445
433
|
team_id: str | None = None,
|
|
446
434
|
global_scope: bool = False,
|
|
447
|
-
**kwargs,
|
|
435
|
+
**kwargs: Any,
|
|
448
436
|
) -> None:
|
|
449
437
|
"""Set specific adapter configuration values.
|
|
450
438
|
|
|
@@ -497,11 +485,13 @@ def set_adapter_config(
|
|
|
497
485
|
|
|
498
486
|
console.print(f"[green]✓[/green] Updated {target_adapter} configuration")
|
|
499
487
|
|
|
500
|
-
# Save config
|
|
488
|
+
# Save config (always to project config for security)
|
|
489
|
+
resolver.save_project_config(config)
|
|
490
|
+
config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
491
|
+
|
|
501
492
|
if global_scope:
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
console.print(f"[dim]Saved to {config_path}[/dim]")
|
|
493
|
+
console.print(
|
|
494
|
+
"[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
console.print(f"[dim]Saved to {config_path}[/dim]")
|
mcp_ticketer/cli/diagnostics.py
CHANGED
|
@@ -12,7 +12,7 @@ from rich.console import Console
|
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def get_config():
|
|
15
|
+
def get_config() -> Any:
|
|
16
16
|
"""Get configuration using the real configuration system."""
|
|
17
17
|
from ..core.config import ConfigurationManager
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ def get_config():
|
|
|
20
20
|
return config_manager.load_config()
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def safe_import_registry():
|
|
23
|
+
def safe_import_registry() -> type:
|
|
24
24
|
"""Safely import adapter registry with fallback."""
|
|
25
25
|
try:
|
|
26
26
|
from ..core.registry import AdapterRegistry
|
|
@@ -30,13 +30,13 @@ def safe_import_registry():
|
|
|
30
30
|
|
|
31
31
|
class MockRegistry:
|
|
32
32
|
@staticmethod
|
|
33
|
-
def get_adapter(adapter_type):
|
|
33
|
+
def get_adapter(adapter_type: str) -> None:
|
|
34
34
|
raise ImportError(f"Adapter {adapter_type} not available")
|
|
35
35
|
|
|
36
36
|
return MockRegistry
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def safe_import_queue_manager():
|
|
39
|
+
def safe_import_queue_manager() -> type:
|
|
40
40
|
"""Safely import worker manager with fallback."""
|
|
41
41
|
try:
|
|
42
42
|
from ..queue.manager import WorkerManager as RealWorkerManager
|
|
@@ -55,16 +55,16 @@ def safe_import_queue_manager():
|
|
|
55
55
|
pass
|
|
56
56
|
|
|
57
57
|
class MockWorkerManager:
|
|
58
|
-
def get_status(self):
|
|
58
|
+
def get_status(self) -> dict[str, Any]:
|
|
59
59
|
return {"running": False, "pid": None, "status": "fallback_mode"}
|
|
60
60
|
|
|
61
|
-
def get_worker_status(self):
|
|
61
|
+
def get_worker_status(self) -> dict[str, Any]:
|
|
62
62
|
return {"running": False, "pid": None, "status": "fallback_mode"}
|
|
63
63
|
|
|
64
|
-
def get_queue_stats(self):
|
|
64
|
+
def get_queue_stats(self) -> dict[str, Any]:
|
|
65
65
|
return {"total": 0, "failed": 0, "pending": 0, "completed": 0}
|
|
66
66
|
|
|
67
|
-
def health_check(self):
|
|
67
|
+
def health_check(self) -> dict[str, Any]:
|
|
68
68
|
return {
|
|
69
69
|
"status": "degraded",
|
|
70
70
|
"score": 50,
|
|
@@ -85,11 +85,11 @@ logger = logging.getLogger(__name__)
|
|
|
85
85
|
class SystemDiagnostics:
|
|
86
86
|
"""Comprehensive system diagnostics and health reporting."""
|
|
87
87
|
|
|
88
|
-
def __init__(self):
|
|
88
|
+
def __init__(self) -> None:
|
|
89
89
|
# Initialize lists first
|
|
90
|
-
self.issues = []
|
|
91
|
-
self.warnings = []
|
|
92
|
-
self.successes = []
|
|
90
|
+
self.issues: list[str] = []
|
|
91
|
+
self.warnings: list[str] = []
|
|
92
|
+
self.successes: list[str] = []
|
|
93
93
|
|
|
94
94
|
try:
|
|
95
95
|
self.config = get_config()
|
|
@@ -611,7 +611,7 @@ class SystemDiagnostics:
|
|
|
611
611
|
|
|
612
612
|
async def _analyze_log_directory(
|
|
613
613
|
self, log_path: Path, log_analysis: dict[str, Any]
|
|
614
|
-
):
|
|
614
|
+
) -> None:
|
|
615
615
|
"""Analyze logs in a specific directory."""
|
|
616
616
|
try:
|
|
617
617
|
for log_file in log_path.glob("*.log"):
|
|
@@ -623,7 +623,9 @@ class SystemDiagnostics:
|
|
|
623
623
|
except Exception as e:
|
|
624
624
|
self.warnings.append(f"Could not analyze logs in {log_path}: {str(e)}")
|
|
625
625
|
|
|
626
|
-
async def _parse_log_file(
|
|
626
|
+
async def _parse_log_file(
|
|
627
|
+
self, log_file: Path, log_analysis: dict[str, Any]
|
|
628
|
+
) -> None:
|
|
627
629
|
"""Parse individual log file for issues."""
|
|
628
630
|
try:
|
|
629
631
|
with open(log_file) as f:
|
|
@@ -672,7 +674,7 @@ class SystemDiagnostics:
|
|
|
672
674
|
|
|
673
675
|
def _generate_recommendations(self) -> list[str]:
|
|
674
676
|
"""Generate actionable recommendations based on diagnosis."""
|
|
675
|
-
recommendations = []
|
|
677
|
+
recommendations: list[str] = []
|
|
676
678
|
|
|
677
679
|
if self.issues:
|
|
678
680
|
recommendations.append(
|
|
@@ -705,7 +707,7 @@ class SystemDiagnostics:
|
|
|
705
707
|
|
|
706
708
|
return recommendations
|
|
707
709
|
|
|
708
|
-
def _display_diagnosis_summary(self, report: dict[str, Any]):
|
|
710
|
+
def _display_diagnosis_summary(self, report: dict[str, Any]) -> None:
|
|
709
711
|
"""Display a comprehensive diagnosis summary."""
|
|
710
712
|
console.print("\n" + "=" * 60)
|
|
711
713
|
console.print("📋 [bold green]DIAGNOSIS SUMMARY[/bold green]")
|