mcp-ticketer 0.1.8__py3-none-any.whl → 0.1.12__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 +1 -1
- mcp_ticketer/adapters/__init__.py +8 -1
- mcp_ticketer/adapters/aitrackdown.py +9 -5
- mcp_ticketer/adapters/github.py +263 -0
- mcp_ticketer/adapters/hybrid.py +505 -0
- mcp_ticketer/adapters/linear.py +220 -0
- mcp_ticketer/cli/configure.py +532 -0
- mcp_ticketer/cli/main.py +107 -0
- mcp_ticketer/cli/migrate_config.py +204 -0
- mcp_ticketer/core/project_config.py +553 -0
- mcp_ticketer/mcp/server.py +349 -15
- mcp_ticketer/queue/queue.py +4 -1
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/METADATA +6 -5
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/RECORD +18 -14
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""Interactive configuration wizard for MCP Ticketer."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.prompt import Prompt, Confirm
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from ..core.project_config import (
|
|
15
|
+
ConfigResolver,
|
|
16
|
+
TicketerConfig,
|
|
17
|
+
AdapterConfig,
|
|
18
|
+
ProjectConfig,
|
|
19
|
+
HybridConfig,
|
|
20
|
+
AdapterType,
|
|
21
|
+
SyncStrategy,
|
|
22
|
+
ConfigValidator
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def configure_wizard() -> None:
|
|
29
|
+
"""Run interactive configuration wizard."""
|
|
30
|
+
console.print(Panel.fit(
|
|
31
|
+
"[bold cyan]MCP-Ticketer Configuration Wizard[/bold cyan]\n"
|
|
32
|
+
"Configure your ticketing system integration",
|
|
33
|
+
border_style="cyan"
|
|
34
|
+
))
|
|
35
|
+
|
|
36
|
+
# Step 1: Choose integration mode
|
|
37
|
+
console.print("\n[bold]Step 1: Integration Mode[/bold]")
|
|
38
|
+
console.print("1. Single Adapter (recommended for most projects)")
|
|
39
|
+
console.print("2. Hybrid Mode (sync across multiple platforms)")
|
|
40
|
+
|
|
41
|
+
mode = Prompt.ask(
|
|
42
|
+
"Select mode",
|
|
43
|
+
choices=["1", "2"],
|
|
44
|
+
default="1"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if mode == "1":
|
|
48
|
+
config = _configure_single_adapter()
|
|
49
|
+
else:
|
|
50
|
+
config = _configure_hybrid_mode()
|
|
51
|
+
|
|
52
|
+
# Step 2: Choose where to save
|
|
53
|
+
console.print("\n[bold]Step 2: Configuration Scope[/bold]")
|
|
54
|
+
console.print("1. Global (all projects): ~/.mcp-ticketer/config.json")
|
|
55
|
+
console.print("2. Project-specific: .mcp-ticketer/config.json in project root")
|
|
56
|
+
|
|
57
|
+
scope = Prompt.ask(
|
|
58
|
+
"Save configuration as",
|
|
59
|
+
choices=["1", "2"],
|
|
60
|
+
default="2"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
resolver = ConfigResolver()
|
|
64
|
+
|
|
65
|
+
if scope == "1":
|
|
66
|
+
# Save global
|
|
67
|
+
resolver.save_global_config(config)
|
|
68
|
+
console.print(f"\n[green]✓[/green] Configuration saved globally to {resolver.GLOBAL_CONFIG_PATH}")
|
|
69
|
+
else:
|
|
70
|
+
# Save project-specific
|
|
71
|
+
resolver.save_project_config(config)
|
|
72
|
+
config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
73
|
+
console.print(f"\n[green]✓[/green] Configuration saved to {config_path}")
|
|
74
|
+
|
|
75
|
+
# Show usage instructions
|
|
76
|
+
console.print("\n[bold]Usage:[/bold]")
|
|
77
|
+
console.print(" CLI: [cyan]mcp-ticketer create \"Task title\"[/cyan]")
|
|
78
|
+
console.print(" MCP: Configure Claude Desktop to use this adapter")
|
|
79
|
+
console.print("\nRun [cyan]mcp-ticketer configure --show[/cyan] to view your configuration")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _configure_single_adapter() -> TicketerConfig:
|
|
83
|
+
"""Configure a single adapter."""
|
|
84
|
+
console.print("\n[bold]Select Ticketing System:[/bold]")
|
|
85
|
+
console.print("1. Linear (Modern project management)")
|
|
86
|
+
console.print("2. JIRA (Enterprise issue tracking)")
|
|
87
|
+
console.print("3. GitHub Issues (Code-integrated tracking)")
|
|
88
|
+
console.print("4. Internal/AITrackdown (File-based, no API)")
|
|
89
|
+
|
|
90
|
+
adapter_choice = Prompt.ask(
|
|
91
|
+
"Select system",
|
|
92
|
+
choices=["1", "2", "3", "4"],
|
|
93
|
+
default="1"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
adapter_type_map = {
|
|
97
|
+
"1": AdapterType.LINEAR,
|
|
98
|
+
"2": AdapterType.JIRA,
|
|
99
|
+
"3": AdapterType.GITHUB,
|
|
100
|
+
"4": AdapterType.AITRACKDOWN,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
adapter_type = adapter_type_map[adapter_choice]
|
|
104
|
+
|
|
105
|
+
# Configure the selected adapter
|
|
106
|
+
if adapter_type == AdapterType.LINEAR:
|
|
107
|
+
adapter_config = _configure_linear()
|
|
108
|
+
elif adapter_type == AdapterType.JIRA:
|
|
109
|
+
adapter_config = _configure_jira()
|
|
110
|
+
elif adapter_type == AdapterType.GITHUB:
|
|
111
|
+
adapter_config = _configure_github()
|
|
112
|
+
else:
|
|
113
|
+
adapter_config = _configure_aitrackdown()
|
|
114
|
+
|
|
115
|
+
# Create config
|
|
116
|
+
config = TicketerConfig(
|
|
117
|
+
default_adapter=adapter_type.value,
|
|
118
|
+
adapters={adapter_type.value: adapter_config}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return config
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _configure_linear() -> AdapterConfig:
|
|
125
|
+
"""Configure Linear adapter."""
|
|
126
|
+
console.print("\n[bold]Configure Linear Integration:[/bold]")
|
|
127
|
+
|
|
128
|
+
# API Key
|
|
129
|
+
api_key = os.getenv("LINEAR_API_KEY") or ""
|
|
130
|
+
if api_key:
|
|
131
|
+
console.print(f"[dim]Found LINEAR_API_KEY in environment[/dim]")
|
|
132
|
+
use_env = Confirm.ask("Use this API key?", default=True)
|
|
133
|
+
if not use_env:
|
|
134
|
+
api_key = ""
|
|
135
|
+
|
|
136
|
+
if not api_key:
|
|
137
|
+
api_key = Prompt.ask(
|
|
138
|
+
"Linear API Key",
|
|
139
|
+
password=True
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Team ID
|
|
143
|
+
team_id = Prompt.ask(
|
|
144
|
+
"Team ID (optional, e.g., team-abc)",
|
|
145
|
+
default=""
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Team Key
|
|
149
|
+
team_key = Prompt.ask(
|
|
150
|
+
"Team Key (optional, e.g., ENG)",
|
|
151
|
+
default=""
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Project ID
|
|
155
|
+
project_id = Prompt.ask(
|
|
156
|
+
"Project ID (optional)",
|
|
157
|
+
default=""
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
config_dict = {
|
|
161
|
+
"adapter": AdapterType.LINEAR.value,
|
|
162
|
+
"api_key": api_key,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if team_id:
|
|
166
|
+
config_dict["team_id"] = team_id
|
|
167
|
+
if team_key:
|
|
168
|
+
config_dict["team_key"] = team_key
|
|
169
|
+
if project_id:
|
|
170
|
+
config_dict["project_id"] = project_id
|
|
171
|
+
|
|
172
|
+
# Validate
|
|
173
|
+
is_valid, error = ConfigValidator.validate_linear_config(config_dict)
|
|
174
|
+
if not is_valid:
|
|
175
|
+
console.print(f"[red]Configuration error: {error}[/red]")
|
|
176
|
+
raise typer.Exit(1)
|
|
177
|
+
|
|
178
|
+
return AdapterConfig.from_dict(config_dict)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _configure_jira() -> AdapterConfig:
|
|
182
|
+
"""Configure JIRA adapter."""
|
|
183
|
+
console.print("\n[bold]Configure JIRA Integration:[/bold]")
|
|
184
|
+
|
|
185
|
+
# Server URL
|
|
186
|
+
server = os.getenv("JIRA_SERVER") or ""
|
|
187
|
+
if not server:
|
|
188
|
+
server = Prompt.ask(
|
|
189
|
+
"JIRA Server URL (e.g., https://company.atlassian.net)"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Email
|
|
193
|
+
email = os.getenv("JIRA_EMAIL") or ""
|
|
194
|
+
if not email:
|
|
195
|
+
email = Prompt.ask("JIRA User Email")
|
|
196
|
+
|
|
197
|
+
# API Token
|
|
198
|
+
api_token = os.getenv("JIRA_API_TOKEN") or ""
|
|
199
|
+
if not api_token:
|
|
200
|
+
console.print("[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]")
|
|
201
|
+
api_token = Prompt.ask(
|
|
202
|
+
"JIRA API Token",
|
|
203
|
+
password=True
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Project Key
|
|
207
|
+
project_key = Prompt.ask(
|
|
208
|
+
"Default Project Key (optional, e.g., PROJ)",
|
|
209
|
+
default=""
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
config_dict = {
|
|
213
|
+
"adapter": AdapterType.JIRA.value,
|
|
214
|
+
"server": server.rstrip('/'),
|
|
215
|
+
"email": email,
|
|
216
|
+
"api_token": api_token,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if project_key:
|
|
220
|
+
config_dict["project_key"] = project_key
|
|
221
|
+
|
|
222
|
+
# Validate
|
|
223
|
+
is_valid, error = ConfigValidator.validate_jira_config(config_dict)
|
|
224
|
+
if not is_valid:
|
|
225
|
+
console.print(f"[red]Configuration error: {error}[/red]")
|
|
226
|
+
raise typer.Exit(1)
|
|
227
|
+
|
|
228
|
+
return AdapterConfig.from_dict(config_dict)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _configure_github() -> AdapterConfig:
|
|
232
|
+
"""Configure GitHub adapter."""
|
|
233
|
+
console.print("\n[bold]Configure GitHub Integration:[/bold]")
|
|
234
|
+
|
|
235
|
+
# Token
|
|
236
|
+
token = os.getenv("GITHUB_TOKEN") or ""
|
|
237
|
+
if token:
|
|
238
|
+
console.print(f"[dim]Found GITHUB_TOKEN in environment[/dim]")
|
|
239
|
+
use_env = Confirm.ask("Use this token?", default=True)
|
|
240
|
+
if not use_env:
|
|
241
|
+
token = ""
|
|
242
|
+
|
|
243
|
+
if not token:
|
|
244
|
+
console.print("[dim]Create token at: https://github.com/settings/tokens/new[/dim]")
|
|
245
|
+
console.print("[dim]Required scopes: repo (or public_repo for public repos)[/dim]")
|
|
246
|
+
token = Prompt.ask(
|
|
247
|
+
"GitHub Personal Access Token",
|
|
248
|
+
password=True
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Repository Owner
|
|
252
|
+
owner = os.getenv("GITHUB_OWNER") or ""
|
|
253
|
+
if not owner:
|
|
254
|
+
owner = Prompt.ask("Repository Owner (username or org)")
|
|
255
|
+
|
|
256
|
+
# Repository Name
|
|
257
|
+
repo = os.getenv("GITHUB_REPO") or ""
|
|
258
|
+
if not repo:
|
|
259
|
+
repo = Prompt.ask("Repository Name")
|
|
260
|
+
|
|
261
|
+
config_dict = {
|
|
262
|
+
"adapter": AdapterType.GITHUB.value,
|
|
263
|
+
"token": token,
|
|
264
|
+
"owner": owner,
|
|
265
|
+
"repo": repo,
|
|
266
|
+
"project_id": f"{owner}/{repo}", # Convenience field
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Validate
|
|
270
|
+
is_valid, error = ConfigValidator.validate_github_config(config_dict)
|
|
271
|
+
if not is_valid:
|
|
272
|
+
console.print(f"[red]Configuration error: {error}[/red]")
|
|
273
|
+
raise typer.Exit(1)
|
|
274
|
+
|
|
275
|
+
return AdapterConfig.from_dict(config_dict)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _configure_aitrackdown() -> AdapterConfig:
|
|
279
|
+
"""Configure AITrackdown adapter."""
|
|
280
|
+
console.print("\n[bold]Configure AITrackdown (File-based):[/bold]")
|
|
281
|
+
|
|
282
|
+
base_path = Prompt.ask(
|
|
283
|
+
"Base path for ticket storage",
|
|
284
|
+
default=".aitrackdown"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
config_dict = {
|
|
288
|
+
"adapter": AdapterType.AITRACKDOWN.value,
|
|
289
|
+
"base_path": base_path,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return AdapterConfig.from_dict(config_dict)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _configure_hybrid_mode() -> TicketerConfig:
|
|
296
|
+
"""Configure hybrid mode with multiple adapters."""
|
|
297
|
+
console.print("\n[bold]Hybrid Mode Configuration[/bold]")
|
|
298
|
+
console.print("Sync tickets across multiple platforms")
|
|
299
|
+
|
|
300
|
+
# Select adapters
|
|
301
|
+
console.print("\n[bold]Select adapters to sync (comma-separated):[/bold]")
|
|
302
|
+
console.print("1. Linear")
|
|
303
|
+
console.print("2. JIRA")
|
|
304
|
+
console.print("3. GitHub")
|
|
305
|
+
console.print("4. AITrackdown")
|
|
306
|
+
|
|
307
|
+
selections = Prompt.ask(
|
|
308
|
+
"Select adapters (e.g., 1,3 for Linear and GitHub)",
|
|
309
|
+
default="1,3"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
adapter_choices = [s.strip() for s in selections.split(",")]
|
|
313
|
+
|
|
314
|
+
adapter_type_map = {
|
|
315
|
+
"1": AdapterType.LINEAR,
|
|
316
|
+
"2": AdapterType.JIRA,
|
|
317
|
+
"3": AdapterType.GITHUB,
|
|
318
|
+
"4": AdapterType.AITRACKDOWN,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
selected_adapters = [adapter_type_map[c] for c in adapter_choices if c in adapter_type_map]
|
|
322
|
+
|
|
323
|
+
if len(selected_adapters) < 2:
|
|
324
|
+
console.print("[red]Hybrid mode requires at least 2 adapters[/red]")
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
|
|
327
|
+
# Configure each adapter
|
|
328
|
+
adapters = {}
|
|
329
|
+
for adapter_type in selected_adapters:
|
|
330
|
+
console.print(f"\n[cyan]Configuring {adapter_type.value}...[/cyan]")
|
|
331
|
+
|
|
332
|
+
if adapter_type == AdapterType.LINEAR:
|
|
333
|
+
adapter_config = _configure_linear()
|
|
334
|
+
elif adapter_type == AdapterType.JIRA:
|
|
335
|
+
adapter_config = _configure_jira()
|
|
336
|
+
elif adapter_type == AdapterType.GITHUB:
|
|
337
|
+
adapter_config = _configure_github()
|
|
338
|
+
else:
|
|
339
|
+
adapter_config = _configure_aitrackdown()
|
|
340
|
+
|
|
341
|
+
adapters[adapter_type.value] = adapter_config
|
|
342
|
+
|
|
343
|
+
# Select primary adapter
|
|
344
|
+
console.print("\n[bold]Select primary adapter (source of truth):[/bold]")
|
|
345
|
+
for idx, adapter_type in enumerate(selected_adapters, 1):
|
|
346
|
+
console.print(f"{idx}. {adapter_type.value}")
|
|
347
|
+
|
|
348
|
+
primary_idx = int(Prompt.ask(
|
|
349
|
+
"Primary adapter",
|
|
350
|
+
choices=[str(i) for i in range(1, len(selected_adapters) + 1)],
|
|
351
|
+
default="1"
|
|
352
|
+
))
|
|
353
|
+
|
|
354
|
+
primary_adapter = selected_adapters[primary_idx - 1].value
|
|
355
|
+
|
|
356
|
+
# Select sync strategy
|
|
357
|
+
console.print("\n[bold]Select sync strategy:[/bold]")
|
|
358
|
+
console.print("1. Primary Source (one-way: primary → others)")
|
|
359
|
+
console.print("2. Bidirectional (two-way sync)")
|
|
360
|
+
console.print("3. Mirror (clone tickets across all)")
|
|
361
|
+
|
|
362
|
+
strategy_choice = Prompt.ask(
|
|
363
|
+
"Sync strategy",
|
|
364
|
+
choices=["1", "2", "3"],
|
|
365
|
+
default="1"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
strategy_map = {
|
|
369
|
+
"1": SyncStrategy.PRIMARY_SOURCE,
|
|
370
|
+
"2": SyncStrategy.BIDIRECTIONAL,
|
|
371
|
+
"3": SyncStrategy.MIRROR,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
sync_strategy = strategy_map[strategy_choice]
|
|
375
|
+
|
|
376
|
+
# Create hybrid config
|
|
377
|
+
hybrid_config = HybridConfig(
|
|
378
|
+
enabled=True,
|
|
379
|
+
adapters=[a.value for a in selected_adapters],
|
|
380
|
+
primary_adapter=primary_adapter,
|
|
381
|
+
sync_strategy=sync_strategy
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Create full config
|
|
385
|
+
config = TicketerConfig(
|
|
386
|
+
default_adapter=primary_adapter,
|
|
387
|
+
adapters=adapters,
|
|
388
|
+
hybrid_mode=hybrid_config
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return config
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def show_current_config() -> None:
|
|
395
|
+
"""Show current configuration."""
|
|
396
|
+
resolver = ConfigResolver()
|
|
397
|
+
|
|
398
|
+
# Try to load configs
|
|
399
|
+
global_config = resolver.load_global_config()
|
|
400
|
+
project_config = resolver.load_project_config()
|
|
401
|
+
|
|
402
|
+
console.print("[bold]Current Configuration:[/bold]\n")
|
|
403
|
+
|
|
404
|
+
# Global config
|
|
405
|
+
if resolver.GLOBAL_CONFIG_PATH.exists():
|
|
406
|
+
console.print(f"[cyan]Global:[/cyan] {resolver.GLOBAL_CONFIG_PATH}")
|
|
407
|
+
console.print(f" Default adapter: {global_config.default_adapter}")
|
|
408
|
+
|
|
409
|
+
if global_config.adapters:
|
|
410
|
+
table = Table(title="Global Adapters")
|
|
411
|
+
table.add_column("Adapter", style="cyan")
|
|
412
|
+
table.add_column("Configured", style="green")
|
|
413
|
+
|
|
414
|
+
for name, config in global_config.adapters.items():
|
|
415
|
+
configured = "✓" if config.enabled else "✗"
|
|
416
|
+
table.add_row(name, configured)
|
|
417
|
+
|
|
418
|
+
console.print(table)
|
|
419
|
+
else:
|
|
420
|
+
console.print("[yellow]No global configuration found[/yellow]")
|
|
421
|
+
|
|
422
|
+
# Project config
|
|
423
|
+
console.print()
|
|
424
|
+
project_config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
425
|
+
if project_config_path.exists():
|
|
426
|
+
console.print(f"[cyan]Project:[/cyan] {project_config_path}")
|
|
427
|
+
if project_config:
|
|
428
|
+
console.print(f" Default adapter: {project_config.default_adapter}")
|
|
429
|
+
|
|
430
|
+
if project_config.adapters:
|
|
431
|
+
table = Table(title="Project Adapters")
|
|
432
|
+
table.add_column("Adapter", style="cyan")
|
|
433
|
+
table.add_column("Configured", style="green")
|
|
434
|
+
|
|
435
|
+
for name, config in project_config.adapters.items():
|
|
436
|
+
configured = "✓" if config.enabled else "✗"
|
|
437
|
+
table.add_row(name, configured)
|
|
438
|
+
|
|
439
|
+
console.print(table)
|
|
440
|
+
|
|
441
|
+
if project_config.hybrid_mode and project_config.hybrid_mode.enabled:
|
|
442
|
+
console.print("\n[bold]Hybrid Mode:[/bold] Enabled")
|
|
443
|
+
console.print(f" Adapters: {', '.join(project_config.hybrid_mode.adapters)}")
|
|
444
|
+
console.print(f" Primary: {project_config.hybrid_mode.primary_adapter}")
|
|
445
|
+
console.print(f" Strategy: {project_config.hybrid_mode.sync_strategy.value}")
|
|
446
|
+
else:
|
|
447
|
+
console.print("[yellow]No project-specific configuration found[/yellow]")
|
|
448
|
+
|
|
449
|
+
# Show resolved config for current project
|
|
450
|
+
console.print("\n[bold]Resolved Configuration (for current project):[/bold]")
|
|
451
|
+
resolved = resolver.resolve_adapter_config()
|
|
452
|
+
|
|
453
|
+
table = Table()
|
|
454
|
+
table.add_column("Key", style="cyan")
|
|
455
|
+
table.add_column("Value", style="white")
|
|
456
|
+
|
|
457
|
+
for key, value in resolved.items():
|
|
458
|
+
# Hide sensitive values
|
|
459
|
+
if any(s in key.lower() for s in ["token", "key", "password"]) and value:
|
|
460
|
+
value = "***"
|
|
461
|
+
table.add_row(key, str(value))
|
|
462
|
+
|
|
463
|
+
console.print(table)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def set_adapter_config(
|
|
467
|
+
adapter: Optional[str] = None,
|
|
468
|
+
api_key: Optional[str] = None,
|
|
469
|
+
project_id: Optional[str] = None,
|
|
470
|
+
team_id: Optional[str] = None,
|
|
471
|
+
global_scope: bool = False,
|
|
472
|
+
**kwargs
|
|
473
|
+
) -> None:
|
|
474
|
+
"""Set specific adapter configuration values.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
adapter: Adapter type to set as default
|
|
478
|
+
api_key: API key/token
|
|
479
|
+
project_id: Project ID
|
|
480
|
+
team_id: Team ID (Linear)
|
|
481
|
+
global_scope: Save to global config instead of project
|
|
482
|
+
**kwargs: Additional adapter-specific options
|
|
483
|
+
"""
|
|
484
|
+
resolver = ConfigResolver()
|
|
485
|
+
|
|
486
|
+
# Load appropriate config
|
|
487
|
+
if global_scope:
|
|
488
|
+
config = resolver.load_global_config()
|
|
489
|
+
else:
|
|
490
|
+
config = resolver.load_project_config() or TicketerConfig()
|
|
491
|
+
|
|
492
|
+
# Update default adapter
|
|
493
|
+
if adapter:
|
|
494
|
+
config.default_adapter = adapter
|
|
495
|
+
console.print(f"[green]✓[/green] Default adapter set to: {adapter}")
|
|
496
|
+
|
|
497
|
+
# Update adapter-specific settings
|
|
498
|
+
updates = {}
|
|
499
|
+
if api_key:
|
|
500
|
+
updates["api_key"] = api_key
|
|
501
|
+
if project_id:
|
|
502
|
+
updates["project_id"] = project_id
|
|
503
|
+
if team_id:
|
|
504
|
+
updates["team_id"] = team_id
|
|
505
|
+
|
|
506
|
+
updates.update(kwargs)
|
|
507
|
+
|
|
508
|
+
if updates:
|
|
509
|
+
target_adapter = adapter or config.default_adapter
|
|
510
|
+
|
|
511
|
+
# Get or create adapter config
|
|
512
|
+
if target_adapter not in config.adapters:
|
|
513
|
+
config.adapters[target_adapter] = AdapterConfig(
|
|
514
|
+
adapter=target_adapter,
|
|
515
|
+
**updates
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
# Update existing
|
|
519
|
+
existing = config.adapters[target_adapter].to_dict()
|
|
520
|
+
existing.update(updates)
|
|
521
|
+
config.adapters[target_adapter] = AdapterConfig.from_dict(existing)
|
|
522
|
+
|
|
523
|
+
console.print(f"[green]✓[/green] Updated {target_adapter} configuration")
|
|
524
|
+
|
|
525
|
+
# Save config
|
|
526
|
+
if global_scope:
|
|
527
|
+
resolver.save_global_config(config)
|
|
528
|
+
console.print(f"[dim]Saved to {resolver.GLOBAL_CONFIG_PATH}[/dim]")
|
|
529
|
+
else:
|
|
530
|
+
resolver.save_project_config(config)
|
|
531
|
+
config_path = resolver.project_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
532
|
+
console.print(f"[dim]Saved to {config_path}[/dim]")
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -18,6 +18,9 @@ from ..core.models import SearchQuery
|
|
|
18
18
|
from ..adapters import AITrackdownAdapter
|
|
19
19
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
20
20
|
from .queue_commands import app as queue_app
|
|
21
|
+
from ..__version__ import __version__
|
|
22
|
+
from .configure import configure_wizard, show_current_config, set_adapter_config
|
|
23
|
+
from .migrate_config import migrate_config_command
|
|
21
24
|
|
|
22
25
|
# Load environment variables
|
|
23
26
|
load_dotenv()
|
|
@@ -29,6 +32,31 @@ app = typer.Typer(
|
|
|
29
32
|
)
|
|
30
33
|
console = Console()
|
|
31
34
|
|
|
35
|
+
|
|
36
|
+
def version_callback(value: bool):
|
|
37
|
+
"""Print version and exit."""
|
|
38
|
+
if value:
|
|
39
|
+
console.print(f"mcp-ticketer version {__version__}")
|
|
40
|
+
raise typer.Exit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback()
|
|
44
|
+
def main_callback(
|
|
45
|
+
version: bool = typer.Option(
|
|
46
|
+
None,
|
|
47
|
+
"--version",
|
|
48
|
+
"-v",
|
|
49
|
+
callback=version_callback,
|
|
50
|
+
is_eager=True,
|
|
51
|
+
help="Show version and exit"
|
|
52
|
+
),
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
MCP Ticketer - Universal ticket management interface.
|
|
56
|
+
"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
32
60
|
# Configuration file management
|
|
33
61
|
CONFIG_FILE = Path.home() / ".mcp-ticketer" / "config.json"
|
|
34
62
|
|
|
@@ -398,6 +426,85 @@ def set_config(
|
|
|
398
426
|
console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
|
|
399
427
|
|
|
400
428
|
|
|
429
|
+
@app.command("configure")
|
|
430
|
+
def configure_command(
|
|
431
|
+
show: bool = typer.Option(
|
|
432
|
+
False,
|
|
433
|
+
"--show",
|
|
434
|
+
help="Show current configuration"
|
|
435
|
+
),
|
|
436
|
+
adapter: Optional[str] = typer.Option(
|
|
437
|
+
None,
|
|
438
|
+
"--adapter",
|
|
439
|
+
help="Set default adapter type"
|
|
440
|
+
),
|
|
441
|
+
api_key: Optional[str] = typer.Option(
|
|
442
|
+
None,
|
|
443
|
+
"--api-key",
|
|
444
|
+
help="Set API key/token"
|
|
445
|
+
),
|
|
446
|
+
project_id: Optional[str] = typer.Option(
|
|
447
|
+
None,
|
|
448
|
+
"--project-id",
|
|
449
|
+
help="Set project ID"
|
|
450
|
+
),
|
|
451
|
+
team_id: Optional[str] = typer.Option(
|
|
452
|
+
None,
|
|
453
|
+
"--team-id",
|
|
454
|
+
help="Set team ID (Linear)"
|
|
455
|
+
),
|
|
456
|
+
global_scope: bool = typer.Option(
|
|
457
|
+
False,
|
|
458
|
+
"--global",
|
|
459
|
+
"-g",
|
|
460
|
+
help="Save to global config instead of project-specific"
|
|
461
|
+
),
|
|
462
|
+
) -> None:
|
|
463
|
+
"""Configure MCP Ticketer integration.
|
|
464
|
+
|
|
465
|
+
Run without arguments to launch interactive wizard.
|
|
466
|
+
Use --show to display current configuration.
|
|
467
|
+
Use options to set specific values directly.
|
|
468
|
+
"""
|
|
469
|
+
# Show configuration
|
|
470
|
+
if show:
|
|
471
|
+
show_current_config()
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# Direct configuration
|
|
475
|
+
if any([adapter, api_key, project_id, team_id]):
|
|
476
|
+
set_adapter_config(
|
|
477
|
+
adapter=adapter,
|
|
478
|
+
api_key=api_key,
|
|
479
|
+
project_id=project_id,
|
|
480
|
+
team_id=team_id,
|
|
481
|
+
global_scope=global_scope
|
|
482
|
+
)
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
# Run interactive wizard
|
|
486
|
+
configure_wizard()
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@app.command("migrate-config")
|
|
490
|
+
def migrate_config(
|
|
491
|
+
dry_run: bool = typer.Option(
|
|
492
|
+
False,
|
|
493
|
+
"--dry-run",
|
|
494
|
+
help="Show what would be done without making changes"
|
|
495
|
+
),
|
|
496
|
+
) -> None:
|
|
497
|
+
"""Migrate configuration from old format to new format.
|
|
498
|
+
|
|
499
|
+
This command will:
|
|
500
|
+
1. Detect old configuration format
|
|
501
|
+
2. Convert to new schema
|
|
502
|
+
3. Backup old config
|
|
503
|
+
4. Apply new config
|
|
504
|
+
"""
|
|
505
|
+
migrate_config_command(dry_run=dry_run)
|
|
506
|
+
|
|
507
|
+
|
|
401
508
|
@app.command("status")
|
|
402
509
|
def status_command():
|
|
403
510
|
"""Show queue and worker status."""
|