soothe-cli 0.1.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.
Files changed (107) hide show
  1. soothe_cli/__init__.py +5 -0
  2. soothe_cli/cli/__init__.py +1 -0
  3. soothe_cli/cli/commands/__init__.py +1 -0
  4. soothe_cli/cli/commands/autopilot_cmd.py +410 -0
  5. soothe_cli/cli/commands/config_cmd.py +277 -0
  6. soothe_cli/cli/commands/run_cmd.py +87 -0
  7. soothe_cli/cli/commands/status_cmd.py +121 -0
  8. soothe_cli/cli/commands/subagent_names.py +17 -0
  9. soothe_cli/cli/commands/thread_cmd.py +657 -0
  10. soothe_cli/cli/execution/__init__.py +6 -0
  11. soothe_cli/cli/execution/daemon.py +194 -0
  12. soothe_cli/cli/execution/headless.py +99 -0
  13. soothe_cli/cli/execution/launcher.py +31 -0
  14. soothe_cli/cli/main.py +509 -0
  15. soothe_cli/cli/renderer.py +444 -0
  16. soothe_cli/cli/stream/__init__.py +17 -0
  17. soothe_cli/cli/stream/context.py +138 -0
  18. soothe_cli/cli/stream/display_line.py +83 -0
  19. soothe_cli/cli/stream/formatter.py +412 -0
  20. soothe_cli/cli/stream/pipeline.py +521 -0
  21. soothe_cli/cli/utils.py +46 -0
  22. soothe_cli/config/__init__.py +5 -0
  23. soothe_cli/config/cli_config.py +155 -0
  24. soothe_cli/plan/__init__.py +5 -0
  25. soothe_cli/plan/rich_tree.py +54 -0
  26. soothe_cli/shared/__init__.py +107 -0
  27. soothe_cli/shared/command_router.py +246 -0
  28. soothe_cli/shared/config_loader.py +68 -0
  29. soothe_cli/shared/display_policy.py +413 -0
  30. soothe_cli/shared/essential_events.py +68 -0
  31. soothe_cli/shared/event_processor.py +823 -0
  32. soothe_cli/shared/message_processing.py +393 -0
  33. soothe_cli/shared/presentation_engine.py +173 -0
  34. soothe_cli/shared/processor_state.py +80 -0
  35. soothe_cli/shared/renderer_protocol.py +158 -0
  36. soothe_cli/shared/rendering.py +43 -0
  37. soothe_cli/shared/slash_commands.py +354 -0
  38. soothe_cli/shared/subagent_routing.py +63 -0
  39. soothe_cli/shared/suppression_state.py +188 -0
  40. soothe_cli/shared/tool_formatters/__init__.py +27 -0
  41. soothe_cli/shared/tool_formatters/base.py +109 -0
  42. soothe_cli/shared/tool_formatters/execution.py +297 -0
  43. soothe_cli/shared/tool_formatters/fallback.py +128 -0
  44. soothe_cli/shared/tool_formatters/file_ops.py +299 -0
  45. soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
  46. soothe_cli/shared/tool_formatters/media.py +291 -0
  47. soothe_cli/shared/tool_formatters/structured.py +202 -0
  48. soothe_cli/shared/tool_formatters/web.py +143 -0
  49. soothe_cli/shared/tool_output_formatter.py +227 -0
  50. soothe_cli/shared/tui_trace_log.py +40 -0
  51. soothe_cli/tui/__init__.py +5 -0
  52. soothe_cli/tui/_ask_user_types.py +50 -0
  53. soothe_cli/tui/_cli_context.py +27 -0
  54. soothe_cli/tui/_env_vars.py +56 -0
  55. soothe_cli/tui/_session_stats.py +114 -0
  56. soothe_cli/tui/_version.py +21 -0
  57. soothe_cli/tui/app.py +4992 -0
  58. soothe_cli/tui/app.tcss +302 -0
  59. soothe_cli/tui/command_registry.py +310 -0
  60. soothe_cli/tui/config.py +2381 -0
  61. soothe_cli/tui/daemon_session.py +233 -0
  62. soothe_cli/tui/file_ops.py +409 -0
  63. soothe_cli/tui/formatting.py +28 -0
  64. soothe_cli/tui/hooks.py +23 -0
  65. soothe_cli/tui/input.py +782 -0
  66. soothe_cli/tui/media_utils.py +471 -0
  67. soothe_cli/tui/model_config.py +518 -0
  68. soothe_cli/tui/output.py +69 -0
  69. soothe_cli/tui/project_utils.py +188 -0
  70. soothe_cli/tui/sessions.py +1248 -0
  71. soothe_cli/tui/skills/__init__.py +5 -0
  72. soothe_cli/tui/skills/invocation.py +74 -0
  73. soothe_cli/tui/skills/load.py +93 -0
  74. soothe_cli/tui/textual_adapter.py +1430 -0
  75. soothe_cli/tui/theme.py +838 -0
  76. soothe_cli/tui/tool_display.py +297 -0
  77. soothe_cli/tui/unicode_security.py +502 -0
  78. soothe_cli/tui/update_check.py +447 -0
  79. soothe_cli/tui/widgets/__init__.py +9 -0
  80. soothe_cli/tui/widgets/_links.py +63 -0
  81. soothe_cli/tui/widgets/approval.py +430 -0
  82. soothe_cli/tui/widgets/ask_user.py +392 -0
  83. soothe_cli/tui/widgets/autocomplete.py +666 -0
  84. soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
  85. soothe_cli/tui/widgets/autopilot_screen.py +64 -0
  86. soothe_cli/tui/widgets/chat_input.py +1834 -0
  87. soothe_cli/tui/widgets/clipboard.py +128 -0
  88. soothe_cli/tui/widgets/diff.py +240 -0
  89. soothe_cli/tui/widgets/editor.py +140 -0
  90. soothe_cli/tui/widgets/history.py +221 -0
  91. soothe_cli/tui/widgets/loading.py +194 -0
  92. soothe_cli/tui/widgets/mcp_viewer.py +352 -0
  93. soothe_cli/tui/widgets/message_store.py +693 -0
  94. soothe_cli/tui/widgets/messages.py +1720 -0
  95. soothe_cli/tui/widgets/model_selector.py +988 -0
  96. soothe_cli/tui/widgets/notification_settings.py +155 -0
  97. soothe_cli/tui/widgets/status.py +403 -0
  98. soothe_cli/tui/widgets/theme_selector.py +158 -0
  99. soothe_cli/tui/widgets/thread_selector.py +1865 -0
  100. soothe_cli/tui/widgets/tool_renderers.py +148 -0
  101. soothe_cli/tui/widgets/tool_widgets.py +254 -0
  102. soothe_cli/tui/widgets/tools.py +165 -0
  103. soothe_cli/tui/widgets/welcome.py +330 -0
  104. soothe_cli-0.1.0.dist-info/METADATA +100 -0
  105. soothe_cli-0.1.0.dist-info/RECORD +107 -0
  106. soothe_cli-0.1.0.dist-info/WHEEL +4 -0
  107. soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
soothe_cli/cli/main.py ADDED
@@ -0,0 +1,509 @@
1
+ """Main CLI entry point using Typer."""
2
+
3
+ # Load environment variables from .env file BEFORE any langchain imports
4
+ # This is required for LangSmith tracing to be activated at import time
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ from importlib.metadata import version # noqa: E402
10
+ from typing import Annotated, Literal # noqa: E402
11
+
12
+ import typer # noqa: E402
13
+
14
+ app = typer.Typer(
15
+ name="soothe",
16
+ help="Intelligent AI assistant for complex tasks",
17
+ no_args_is_help=False,
18
+ add_completion=False,
19
+ )
20
+
21
+
22
+ def add_help_alias(nested_app: typer.Typer) -> None:
23
+ """Add -h as an alias for --help to a nested Typer app.
24
+
25
+ This is a workaround for Typer not supporting -h for nested command groups.
26
+ Must be called AFTER creating the nested app but BEFORE adding commands.
27
+
28
+ Args:
29
+ nested_app: The nested Typer app to add -h support to.
30
+ """
31
+
32
+ # Add a callback that defines -h option
33
+ @nested_app.callback(invoke_without_command=True)
34
+ def help_callback(
35
+ ctx: typer.Context,
36
+ show_help: Annotated[ # noqa: FBT002
37
+ bool,
38
+ typer.Option("-h", "--help", is_flag=True, help="Show this message and exit."),
39
+ ] = False,
40
+ ) -> None:
41
+ # If -h/--help is passed, show help and exit before command parsing
42
+ if show_help:
43
+ typer.echo(ctx.get_help())
44
+ raise typer.Exit(code=0)
45
+
46
+ # If no subcommand and no help flag, show help by default
47
+ if ctx.invoked_subcommand is None:
48
+ typer.echo(ctx.get_help())
49
+ raise typer.Exit(code=0)
50
+
51
+
52
+ @app.callback(invoke_without_command=True)
53
+ def main(
54
+ ctx: typer.Context,
55
+ config: Annotated[
56
+ str | None,
57
+ typer.Option("--config", "-c", help="Path to configuration file."),
58
+ ] = None,
59
+ prompt: Annotated[
60
+ str | None,
61
+ typer.Option(
62
+ "--prompt", "-p", help="Prompt to send as user message (headless single-shot mode)."
63
+ ),
64
+ ] = None,
65
+ no_tui: Annotated[ # noqa: FBT002
66
+ bool,
67
+ typer.Option("--no-tui", help="Disable TUI; run single prompt and exit."),
68
+ ] = False,
69
+ output_format: Annotated[
70
+ str,
71
+ typer.Option("--format", "-f", help="Output format for headless mode: text or jsonl."),
72
+ ] = "text",
73
+ verbosity: Annotated[
74
+ Literal["quiet", "minimal", "normal", "detailed", "debug"] | None,
75
+ typer.Option(
76
+ "--verbosity",
77
+ "-v",
78
+ help="Verbosity level: quiet, normal, detailed, debug. 'minimal' is accepted as an alias.",
79
+ ),
80
+ ] = None,
81
+ show_help: Annotated[ # noqa: FBT002
82
+ bool,
83
+ typer.Option("--help", "-h", is_flag=True, help="Show this message and exit."),
84
+ ] = False,
85
+ show_version: Annotated[ # noqa: FBT002
86
+ bool,
87
+ typer.Option("--version", is_flag=True, help="Show version and exit."),
88
+ ] = False,
89
+ ) -> None:
90
+ """Soothe CLI - Intelligent AI assistant client.
91
+
92
+ Run without arguments for interactive TUI mode, or provide a prompt via --prompt/-p option.
93
+
94
+ Note: This is the CLI client. Use 'soothe-daemon' command to manage the daemon server.
95
+
96
+ Examples:
97
+ soothe # Interactive TUI mode
98
+ soothe -p "Research AI advances" # Headless single-prompt mode
99
+ soothe --config custom.yml # Use custom CLI config
100
+ soothe thread list # List conversation threads
101
+ """
102
+ # Handle -h/--help flag
103
+ if show_help:
104
+ typer.echo(ctx.get_help())
105
+ raise typer.Exit
106
+
107
+ # Handle --version flag
108
+ if show_version:
109
+ typer.echo(f"soothe {version('soothe-cli')}")
110
+ raise typer.Exit
111
+
112
+ # Only run default behavior if no subcommand is being invoked
113
+ if ctx.invoked_subcommand is None:
114
+ from soothe_cli.cli.commands.run_cmd import run_impl
115
+
116
+ run_impl(
117
+ prompt=prompt,
118
+ config=config,
119
+ thread_id=None,
120
+ no_tui=no_tui,
121
+ autonomous=False,
122
+ max_iterations=None,
123
+ output_format=output_format,
124
+ verbosity=verbosity,
125
+ )
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Thread Command (Nested Subcommands)
130
+ # ---------------------------------------------------------------------------
131
+
132
+ thread_app = typer.Typer(name="thread", help="Manage conversation threads")
133
+ add_help_alias(thread_app)
134
+ app.add_typer(thread_app)
135
+
136
+
137
+ @thread_app.command("list")
138
+ def _thread_list(
139
+ config: Annotated[
140
+ str | None,
141
+ typer.Option("--config", "-c", help="Path to configuration file."),
142
+ ] = None,
143
+ status: Annotated[
144
+ str | None,
145
+ typer.Option("--status", "-s", help="Filter by status (active, archived)."),
146
+ ] = None,
147
+ ) -> None:
148
+ """List all conversation threads.
149
+
150
+ Examples:
151
+ soothe thread list
152
+ soothe thread list --status active
153
+ """
154
+ from soothe_cli.cli.commands.thread_cmd import thread_list
155
+
156
+ thread_list(config=config, status=status)
157
+
158
+
159
+ @thread_app.command("show")
160
+ def _thread_show(
161
+ thread_id: Annotated[str, typer.Argument(help="Thread ID to show.")],
162
+ config: Annotated[
163
+ str | None,
164
+ typer.Option("--config", "-c", help="Path to configuration file."),
165
+ ] = None,
166
+ ) -> None:
167
+ """Show thread details.
168
+
169
+ Example:
170
+ soothe thread show abc123
171
+ """
172
+ from soothe_cli.cli.commands.thread_cmd import thread_show
173
+
174
+ thread_show(thread_id=thread_id, config=config)
175
+
176
+
177
+ @thread_app.command("continue")
178
+ def _thread_continue(
179
+ thread_id: Annotated[
180
+ str | None,
181
+ typer.Argument(help="Thread ID to continue. Omit to continue last active thread."),
182
+ ] = None,
183
+ config: Annotated[
184
+ str | None,
185
+ typer.Option("--config", "-c", help="Path to configuration file."),
186
+ ] = None,
187
+ new: Annotated[ # noqa: FBT002
188
+ bool,
189
+ typer.Option("--new", help="Create a new thread instead of continuing."),
190
+ ] = False,
191
+ ) -> None:
192
+ """Continue a conversation thread in the TUI.
193
+
194
+ Requires a running daemon. Start daemon with 'soothe-daemon start' first.
195
+
196
+ Examples:
197
+ soothe thread continue abc123
198
+ soothe thread continue --new
199
+ soothe thread continue
200
+ """
201
+ from soothe_cli.cli.commands.thread_cmd import thread_continue
202
+
203
+ thread_continue(thread_id=thread_id, config=config, new=new)
204
+
205
+
206
+ @thread_app.command("archive")
207
+ def _thread_archive(
208
+ thread_id: Annotated[str, typer.Argument(help="Thread ID to archive.")],
209
+ config: Annotated[
210
+ str | None,
211
+ typer.Option("--config", "-c", help="Path to configuration file."),
212
+ ] = None,
213
+ ) -> None:
214
+ """Archive a thread.
215
+
216
+ Example:
217
+ soothe thread archive abc123
218
+ """
219
+ from soothe_cli.cli.commands.thread_cmd import thread_archive
220
+
221
+ thread_archive(thread_id=thread_id, config=config)
222
+
223
+
224
+ @thread_app.command("delete")
225
+ def _thread_delete(
226
+ thread_id: Annotated[str, typer.Argument(help="Thread ID to delete.")],
227
+ config: Annotated[
228
+ str | None,
229
+ typer.Option("--config", "-c", help="Path to configuration file."),
230
+ ] = None,
231
+ yes: Annotated[ # noqa: FBT002
232
+ bool,
233
+ typer.Option("--yes", "-y", help="Skip confirmation."),
234
+ ] = False,
235
+ ) -> None:
236
+ """Permanently delete a thread.
237
+
238
+ Example:
239
+ soothe thread delete abc123
240
+ """
241
+ from soothe_cli.cli.commands.thread_cmd import thread_delete
242
+
243
+ thread_delete(thread_id=thread_id, config=config, yes=yes)
244
+
245
+
246
+ @thread_app.command("export")
247
+ def _thread_export(
248
+ thread_id: Annotated[str, typer.Argument(help="Thread ID to export.")],
249
+ output: Annotated[
250
+ str | None,
251
+ typer.Option("--output", "-o", help="Output file path."),
252
+ ] = None,
253
+ export_format: Annotated[
254
+ str,
255
+ typer.Option("--format", "-f", help="Export format: jsonl or md."),
256
+ ] = "jsonl",
257
+ ) -> None:
258
+ """Export thread conversation to a file.
259
+
260
+ Example:
261
+ soothe thread export abc123 --output out.json
262
+ """
263
+ from soothe_cli.cli.commands.thread_cmd import thread_export
264
+
265
+ thread_export(thread_id=thread_id, output=output, export_format=export_format)
266
+
267
+
268
+ @thread_app.command("stats")
269
+ def _thread_stats(
270
+ thread_id: Annotated[str, typer.Argument(help="Thread ID.")],
271
+ config: Annotated[
272
+ str | None,
273
+ typer.Option("--config", "-c", help="Path to configuration file."),
274
+ ] = None,
275
+ ) -> None:
276
+ """Show thread execution statistics.
277
+
278
+ Example:
279
+ soothe thread stats abc123
280
+ """
281
+ from soothe_cli.cli.commands.thread_cmd import thread_stats
282
+
283
+ thread_stats(thread_id=thread_id, config=config)
284
+
285
+
286
+ @thread_app.command("tag")
287
+ def _thread_tag(
288
+ thread_id: Annotated[str, typer.Argument(help="Thread ID.")],
289
+ tags: Annotated[
290
+ list[str],
291
+ typer.Argument(help="Tags to add/remove."),
292
+ ],
293
+ config: Annotated[
294
+ str | None,
295
+ typer.Option("--config", "-c", help="Path to configuration file."),
296
+ ] = None,
297
+ remove: Annotated[ # noqa: FBT002
298
+ bool,
299
+ typer.Option("--remove", help="Remove tags instead of adding."),
300
+ ] = False,
301
+ ) -> None:
302
+ """Add or remove tags from a thread.
303
+
304
+ Examples:
305
+ soothe thread tag abc123 research analysis
306
+ soothe thread tag abc123 research --remove
307
+ """
308
+ from soothe_cli.cli.commands.thread_cmd import thread_tag
309
+
310
+ thread_tag(thread_id=thread_id, tags=tags, config=config, remove=remove)
311
+
312
+
313
+ @thread_app.command("create")
314
+ def _thread_create(
315
+ config: Annotated[
316
+ str | None,
317
+ typer.Option("--config", "-c", help="Path to configuration file."),
318
+ ] = None,
319
+ message: Annotated[
320
+ str | None,
321
+ typer.Option("--message", "-m", help="Initial message to seed the thread."),
322
+ ] = None,
323
+ tag: Annotated[
324
+ list[str] | None,
325
+ typer.Option("--tag", "-t", help="Tags for the thread (repeatable)."),
326
+ ] = None,
327
+ ) -> None:
328
+ """Create a new persisted thread.
329
+
330
+ Examples:
331
+ soothe thread create
332
+ soothe thread create --message "Hello world"
333
+ soothe thread create --tag research
334
+ """
335
+ from soothe_cli.cli.commands.thread_cmd import thread_create
336
+
337
+ thread_create(config=config, message=message, tag=tag)
338
+
339
+
340
+ @thread_app.command("artifacts")
341
+ def _thread_artifacts(
342
+ thread_id: Annotated[str, typer.Argument(help="Thread ID to list artifacts for.")],
343
+ config: Annotated[
344
+ str | None,
345
+ typer.Option("--config", "-c", help="Path to configuration file."),
346
+ ] = None,
347
+ ) -> None:
348
+ """List artifacts for a thread.
349
+
350
+ Example:
351
+ soothe thread artifacts abc123
352
+ """
353
+ from soothe_cli.cli.commands.thread_cmd import thread_artifacts
354
+
355
+ thread_artifacts(thread_id=thread_id, config=config)
356
+
357
+
358
+ # ---------------------------------------------------------------------------
359
+ # Config Command (Nested Subcommands)
360
+ # ---------------------------------------------------------------------------
361
+
362
+ config_app = typer.Typer(name="config", help="Manage Soothe configuration")
363
+ add_help_alias(config_app)
364
+ app.add_typer(config_app)
365
+
366
+
367
+ @config_app.command("show")
368
+ def _config_show(
369
+ config: Annotated[
370
+ str | None,
371
+ typer.Option("--config", "-c", help="Path to configuration file."),
372
+ ] = None,
373
+ format_output: Annotated[
374
+ str,
375
+ typer.Option("--format", "-f", help="Output format: json or summary."),
376
+ ] = "summary",
377
+ show_sensitive: Annotated[ # noqa: FBT002
378
+ bool,
379
+ typer.Option("--show-sensitive", help="Show sensitive values like API keys."),
380
+ ] = False,
381
+ ) -> None:
382
+ """Display current configuration.
383
+
384
+ Examples:
385
+ soothe config show
386
+ soothe config show --show-sensitive
387
+ soothe config show --format json
388
+ """
389
+ from soothe_cli.cli.commands.config_cmd import config_show
390
+
391
+ config_show(config=config, format_output=format_output, show_sensitive=show_sensitive)
392
+
393
+
394
+ @config_app.command("init")
395
+ def _config_init(
396
+ force: Annotated[ # noqa: FBT002
397
+ bool,
398
+ typer.Option(
399
+ "--force", "-f", help="Overwrite existing configuration without confirmation."
400
+ ),
401
+ ] = False,
402
+ ) -> None:
403
+ """Initialize ~/.soothe with a default configuration.
404
+
405
+ Examples:
406
+ soothe config init
407
+ soothe config init --force
408
+ """
409
+ from soothe_cli.cli.commands.config_cmd import config_init
410
+
411
+ config_init(force=force)
412
+
413
+
414
+ @config_app.command("validate")
415
+ def _config_validate(
416
+ config: Annotated[
417
+ str | None,
418
+ typer.Option("--config", "-c", help="Path to configuration file."),
419
+ ] = None,
420
+ ) -> None:
421
+ """Validate configuration file and show basic info.
422
+
423
+ Examples:
424
+ soothe config validate
425
+ soothe config validate --config custom.yml
426
+ """
427
+ from soothe_cli.cli.commands.config_cmd import config_validate
428
+
429
+ config_validate(config=config)
430
+
431
+
432
+ # ---------------------------------------------------------------------------
433
+ # Agent Command (Nested Subcommands)
434
+ # ---------------------------------------------------------------------------
435
+
436
+ agent_app = typer.Typer(name="agent", help="List and manage agents")
437
+ add_help_alias(agent_app)
438
+ app.add_typer(agent_app)
439
+
440
+
441
+ @agent_app.command("list")
442
+ def _agent_list(
443
+ config: Annotated[
444
+ str | None,
445
+ typer.Option("--config", "-c", help="Path to configuration file."),
446
+ ] = None,
447
+ enabled: Annotated[ # noqa: FBT002
448
+ bool,
449
+ typer.Option("--enabled", help="Show only enabled agents."),
450
+ ] = False,
451
+ disabled: Annotated[ # noqa: FBT002
452
+ bool,
453
+ typer.Option("--disabled", help="Show only disabled agents."),
454
+ ] = False,
455
+ ) -> None:
456
+ """List available agents and their status.
457
+
458
+ Examples:
459
+ soothe agent list
460
+ soothe agent list --enabled
461
+ soothe agent list --disabled
462
+ """
463
+ from soothe_cli.cli.commands.status_cmd import agent_list
464
+
465
+ agent_list(config=config, enabled=enabled, disabled=disabled)
466
+
467
+
468
+ @agent_app.command("status")
469
+ def _agent_status(
470
+ config: Annotated[
471
+ str | None,
472
+ typer.Option("--config", "-c", help="Path to configuration file."),
473
+ ] = None,
474
+ ) -> None:
475
+ """Show detailed agent status.
476
+
477
+ Example:
478
+ soothe agent status
479
+ """
480
+ from soothe_cli.cli.commands.status_cmd import agent_status
481
+
482
+ agent_status(config=config)
483
+
484
+
485
+ # ---------------------------------------------------------------------------
486
+ # Autopilot Command (Nested Subcommands)
487
+ # ---------------------------------------------------------------------------
488
+
489
+ from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
490
+
491
+ add_help_alias(_autopilot_app)
492
+ app.add_typer(_autopilot_app, name="autopilot")
493
+
494
+
495
+ # ---------------------------------------------------------------------------
496
+ # Help Command
497
+ # ---------------------------------------------------------------------------
498
+
499
+
500
+ @app.command(name="help")
501
+ def help_command(ctx: typer.Context) -> None:
502
+ """Show help message and exit."""
503
+ # Get the parent context (the main app) to show full help
504
+ parent_ctx = ctx.parent or ctx
505
+ typer.echo(parent_ctx.get_help())
506
+
507
+
508
+ if __name__ == "__main__":
509
+ app()