mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +91 -54
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1544
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -2030
  155. mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
  157. mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,435 @@
1
+ """Ticket instructions management commands.
2
+
3
+ This module implements CLI commands for managing ticket writing instructions,
4
+ allowing users to customize and view the guidelines that help create
5
+ well-structured, consistent tickets.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+ from rich.panel import Panel
15
+
16
+ from ..core.instructions import (
17
+ InstructionsError,
18
+ InstructionsValidationError,
19
+ TicketInstructionsManager,
20
+ )
21
+
22
+ app = typer.Typer(
23
+ name="instructions",
24
+ help="Manage ticket writing instructions for your project",
25
+ )
26
+ console = Console()
27
+
28
+
29
+ @app.command()
30
+ def show(
31
+ default: bool = typer.Option(
32
+ False,
33
+ "--default",
34
+ help="Show default instructions instead of custom",
35
+ ),
36
+ raw: bool = typer.Option(
37
+ False,
38
+ "--raw",
39
+ help="Output raw markdown without formatting",
40
+ ),
41
+ ) -> None:
42
+ """Display current ticket writing instructions.
43
+
44
+ By default, shows custom instructions if they exist, otherwise shows defaults.
45
+ Use --default to always show the default instructions.
46
+ Use --raw to output raw markdown without Rich formatting (useful for piping).
47
+
48
+ Examples:
49
+ --------
50
+ # Show current instructions (custom or default)
51
+ mcp-ticketer instructions show
52
+
53
+ # Always show default instructions
54
+ mcp-ticketer instructions show --default
55
+
56
+ # Output raw markdown for piping
57
+ mcp-ticketer instructions show --raw > team_guide.md
58
+
59
+ """
60
+ try:
61
+ manager = TicketInstructionsManager()
62
+
63
+ if default:
64
+ instructions = manager.get_default_instructions()
65
+ source = "default"
66
+ else:
67
+ instructions = manager.get_instructions()
68
+ source = "custom" if manager.has_custom_instructions() else "default"
69
+
70
+ if raw:
71
+ # Raw output for piping
72
+ console.print(instructions)
73
+ else:
74
+ # Rich formatted output
75
+ if source == "custom":
76
+ title = f"[green]Custom Instructions[/green] ({manager.get_instructions_path()})"
77
+ else:
78
+ title = "[blue]Default Instructions[/blue]"
79
+
80
+ panel = Panel(
81
+ Markdown(instructions),
82
+ title=title,
83
+ border_style="cyan",
84
+ )
85
+ console.print(panel)
86
+
87
+ except InstructionsError as e:
88
+ console.print(f"[red]Error:[/red] {e}")
89
+ raise typer.Exit(1) from None
90
+
91
+
92
+ @app.command()
93
+ def add(
94
+ file_path: str | None = typer.Argument(
95
+ None,
96
+ help="Path to markdown file with custom instructions",
97
+ ),
98
+ stdin: bool = typer.Option(
99
+ False,
100
+ "--stdin",
101
+ help="Read instructions from stdin instead of file",
102
+ ),
103
+ force: bool = typer.Option(
104
+ False,
105
+ "--force",
106
+ "-f",
107
+ help="Overwrite existing custom instructions without confirmation",
108
+ ),
109
+ ) -> None:
110
+ """Add custom ticket writing instructions for your project.
111
+
112
+ You can provide instructions from a file or via stdin. If custom instructions
113
+ already exist, you'll be prompted for confirmation unless --force is used.
114
+
115
+ Examples:
116
+ --------
117
+ # Add from file
118
+ mcp-ticketer instructions add team_guidelines.md
119
+
120
+ # Add from stdin
121
+ cat guidelines.md | mcp-ticketer instructions add --stdin
122
+
123
+ # Force overwrite existing
124
+ mcp-ticketer instructions add new_guide.md --force
125
+
126
+ """
127
+ try:
128
+ manager = TicketInstructionsManager()
129
+
130
+ # Check for existing custom instructions
131
+ if manager.has_custom_instructions() and not force:
132
+ path = manager.get_instructions_path()
133
+ console.print(
134
+ f"[yellow]Warning:[/yellow] Custom instructions already exist at {path}"
135
+ )
136
+
137
+ confirm = typer.confirm("Do you want to overwrite them?")
138
+ if not confirm:
139
+ console.print("[yellow]Operation cancelled[/yellow]")
140
+ raise typer.Exit(0) from None
141
+
142
+ # Get content from stdin or file
143
+ if stdin:
144
+ console.print("[dim]Reading from stdin... (Press Ctrl+D when done)[/dim]")
145
+ content = sys.stdin.read()
146
+ if not content.strip():
147
+ console.print("[red]Error:[/red] No content provided on stdin")
148
+ raise typer.Exit(1) from None
149
+ elif file_path:
150
+ source_path = Path(file_path)
151
+ if not source_path.exists():
152
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
153
+ raise typer.Exit(1) from None
154
+
155
+ try:
156
+ content = source_path.read_text(encoding="utf-8")
157
+ except Exception as e:
158
+ console.print(f"[red]Error:[/red] Failed to read file: {e}")
159
+ raise typer.Exit(1) from None
160
+ else:
161
+ console.print("[red]Error:[/red] Either provide a file path or use --stdin")
162
+ console.print("Example: mcp-ticketer instructions add guidelines.md")
163
+ raise typer.Exit(1) from None
164
+
165
+ # Set instructions
166
+ manager.set_instructions(content)
167
+
168
+ path = manager.get_instructions_path()
169
+ console.print(f"[green]✓[/green] Custom instructions saved to: {path}")
170
+ console.print("[dim]Use 'mcp-ticketer instructions show' to view them[/dim]")
171
+
172
+ except InstructionsValidationError as e:
173
+ console.print(f"[red]Validation Error:[/red] {e}")
174
+ raise typer.Exit(1) from None
175
+ except InstructionsError as e:
176
+ console.print(f"[red]Error:[/red] {e}")
177
+ raise typer.Exit(1) from None
178
+
179
+
180
+ @app.command()
181
+ def update(
182
+ file_path: str | None = typer.Argument(
183
+ None,
184
+ help="Path to markdown file with updated instructions",
185
+ ),
186
+ stdin: bool = typer.Option(
187
+ False,
188
+ "--stdin",
189
+ help="Read instructions from stdin instead of file",
190
+ ),
191
+ ) -> None:
192
+ """Update existing custom instructions (alias for 'add --force').
193
+
194
+ This is a convenience command that overwrites existing custom instructions
195
+ without prompting for confirmation.
196
+
197
+ Examples:
198
+ --------
199
+ # Update from file
200
+ mcp-ticketer instructions update new_guidelines.md
201
+
202
+ # Update from stdin
203
+ cat updated.md | mcp-ticketer instructions update --stdin
204
+
205
+ """
206
+ try:
207
+ manager = TicketInstructionsManager()
208
+
209
+ if not manager.has_custom_instructions():
210
+ console.print("[yellow]Warning:[/yellow] No custom instructions exist yet")
211
+ console.print("Use 'mcp-ticketer instructions add' to create them first")
212
+ raise typer.Exit(1) from None
213
+
214
+ # Get content from stdin or file
215
+ if stdin:
216
+ console.print("[dim]Reading from stdin... (Press Ctrl+D when done)[/dim]")
217
+ content = sys.stdin.read()
218
+ if not content.strip():
219
+ console.print("[red]Error:[/red] No content provided on stdin")
220
+ raise typer.Exit(1) from None
221
+ elif file_path:
222
+ source_path = Path(file_path)
223
+ if not source_path.exists():
224
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
225
+ raise typer.Exit(1) from None
226
+
227
+ try:
228
+ content = source_path.read_text(encoding="utf-8")
229
+ except Exception as e:
230
+ console.print(f"[red]Error:[/red] Failed to read file: {e}")
231
+ raise typer.Exit(1) from None
232
+ else:
233
+ console.print("[red]Error:[/red] Either provide a file path or use --stdin")
234
+ console.print("Example: mcp-ticketer instructions update guidelines.md")
235
+ raise typer.Exit(1) from None
236
+
237
+ # Update instructions (force overwrite)
238
+ manager.set_instructions(content)
239
+
240
+ path = manager.get_instructions_path()
241
+ console.print(f"[green]✓[/green] Custom instructions updated: {path}")
242
+ console.print("[dim]Use 'mcp-ticketer instructions show' to view them[/dim]")
243
+
244
+ except InstructionsValidationError as e:
245
+ console.print(f"[red]Validation Error:[/red] {e}")
246
+ raise typer.Exit(1) from None
247
+ except InstructionsError as e:
248
+ console.print(f"[red]Error:[/red] {e}")
249
+ raise typer.Exit(1) from None
250
+
251
+
252
+ @app.command()
253
+ def delete(
254
+ yes: bool = typer.Option(
255
+ False,
256
+ "--yes",
257
+ "-y",
258
+ help="Skip confirmation prompt",
259
+ ),
260
+ ) -> None:
261
+ """Delete custom instructions and revert to defaults.
262
+
263
+ This removes your project-specific instructions file. After deletion,
264
+ the default instructions will be used.
265
+
266
+ Examples:
267
+ --------
268
+ # Delete with confirmation prompt
269
+ mcp-ticketer instructions delete
270
+
271
+ # Skip confirmation
272
+ mcp-ticketer instructions delete --yes
273
+
274
+ """
275
+ try:
276
+ manager = TicketInstructionsManager()
277
+
278
+ if not manager.has_custom_instructions():
279
+ console.print("[yellow]No custom instructions to delete[/yellow]")
280
+ console.print("[dim]Already using default instructions[/dim]")
281
+ raise typer.Exit(0) from None
282
+
283
+ path = manager.get_instructions_path()
284
+
285
+ if not yes:
286
+ console.print(f"[yellow]Warning:[/yellow] This will delete: {path}")
287
+ console.print("After deletion, default instructions will be used.")
288
+
289
+ confirm = typer.confirm("Are you sure?")
290
+ if not confirm:
291
+ console.print("[yellow]Operation cancelled[/yellow]")
292
+ raise typer.Exit(0) from None
293
+
294
+ # Delete instructions
295
+ manager.delete_instructions()
296
+
297
+ console.print("[green]✓[/green] Custom instructions deleted")
298
+ console.print("[dim]Now using default instructions[/dim]")
299
+
300
+ except InstructionsError as e:
301
+ console.print(f"[red]Error:[/red] {e}")
302
+ raise typer.Exit(1) from None
303
+
304
+
305
+ @app.command()
306
+ def path() -> None:
307
+ """Show path to custom instructions file.
308
+
309
+ Displays the path where custom instructions are (or would be) stored
310
+ for this project, along with status information.
311
+
312
+ Examples:
313
+ --------
314
+ # Show instructions file path
315
+ mcp-ticketer instructions path
316
+
317
+ # Use in scripts
318
+ INST_PATH=$(mcp-ticketer instructions path --quiet)
319
+
320
+ """
321
+ try:
322
+ manager = TicketInstructionsManager()
323
+ inst_path = manager.get_instructions_path()
324
+ exists = manager.has_custom_instructions()
325
+
326
+ console.print(f"[cyan]Instructions file:[/cyan] {inst_path}")
327
+
328
+ if exists:
329
+ console.print("[green]Status:[/green] Custom instructions exist")
330
+
331
+ # Show file size
332
+ try:
333
+ size = inst_path.stat().st_size
334
+ console.print(f"[dim]Size: {size} bytes[/dim]")
335
+ except Exception:
336
+ pass
337
+ else:
338
+ console.print(
339
+ "[yellow]Status:[/yellow] No custom instructions (using defaults)"
340
+ )
341
+ console.print(
342
+ "[dim]Create with: mcp-ticketer instructions add <file>[/dim]"
343
+ )
344
+
345
+ except InstructionsError as e:
346
+ console.print(f"[red]Error:[/red] {e}")
347
+ raise typer.Exit(1) from None
348
+
349
+
350
+ @app.command()
351
+ def edit() -> None:
352
+ """Open instructions in default editor.
353
+
354
+ Opens the custom instructions file in your system's default text editor.
355
+ If custom instructions don't exist yet, creates them with default content
356
+ first.
357
+
358
+ The editor is determined by the EDITOR environment variable, or falls back
359
+ to sensible defaults (vim on Unix, notepad on Windows).
360
+
361
+ Examples:
362
+ --------
363
+ # Edit instructions
364
+ mcp-ticketer instructions edit
365
+
366
+ # Use specific editor
367
+ EDITOR=nano mcp-ticketer instructions edit
368
+
369
+ """
370
+ import os
371
+ import platform
372
+ import subprocess
373
+
374
+ try:
375
+ manager = TicketInstructionsManager()
376
+
377
+ # If no custom instructions exist, create them with defaults
378
+ if not manager.has_custom_instructions():
379
+ console.print("[yellow]No custom instructions yet[/yellow]")
380
+ console.print("[dim]Creating from defaults...[/dim]")
381
+
382
+ # Copy defaults to custom location
383
+ default_content = manager.get_default_instructions()
384
+ manager.set_instructions(default_content)
385
+
386
+ console.print(
387
+ f"[green]✓[/green] Created custom instructions at: {manager.get_instructions_path()}"
388
+ )
389
+
390
+ inst_path = manager.get_instructions_path()
391
+
392
+ # Determine editor
393
+ editor = os.environ.get("EDITOR")
394
+
395
+ if not editor:
396
+ # Platform-specific defaults
397
+ system = platform.system()
398
+ if system == "Windows":
399
+ editor = "notepad"
400
+ else:
401
+ # Unix-like: try common editors
402
+ for candidate in ["vim", "vi", "nano", "emacs"]:
403
+ try:
404
+ result = subprocess.run(
405
+ ["which", candidate],
406
+ capture_output=True,
407
+ text=True,
408
+ timeout=1,
409
+ )
410
+ if result.returncode == 0:
411
+ editor = candidate
412
+ break
413
+ except Exception:
414
+ continue
415
+
416
+ if not editor:
417
+ editor = "vi" # Ultimate fallback
418
+
419
+ console.print(f"[dim]Opening with {editor}...[/dim]")
420
+
421
+ # Open editor
422
+ try:
423
+ subprocess.run([editor, str(inst_path)], check=True)
424
+ console.print(f"[green]✓[/green] Finished editing: {inst_path}")
425
+ except subprocess.CalledProcessError as e:
426
+ console.print(f"[red]Error:[/red] Editor exited with code {e.returncode}")
427
+ raise typer.Exit(1) from None
428
+ except FileNotFoundError:
429
+ console.print(f"[red]Error:[/red] Editor not found: {editor}")
430
+ console.print("Set EDITOR environment variable to your preferred editor")
431
+ raise typer.Exit(1) from None
432
+
433
+ except InstructionsError as e:
434
+ console.print(f"[red]Error:[/red] {e}")
435
+ raise typer.Exit(1) from None