mcli-framework 7.10.1__py3-none-any.whl → 7.11.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 mcli-framework might be problematic. Click here for more details.

Files changed (43) hide show
  1. mcli/app/commands_cmd.py +150 -58
  2. mcli/app/main.py +21 -27
  3. mcli/lib/custom_commands.py +62 -12
  4. mcli/lib/optional_deps.py +240 -0
  5. mcli/lib/paths.py +129 -5
  6. mcli/self/migrate_cmd.py +261 -0
  7. mcli/self/self_cmd.py +8 -0
  8. mcli/workflow/git_commit/ai_service.py +13 -2
  9. mcli/workflow/notebook/__init__.py +16 -0
  10. mcli/workflow/notebook/converter.py +375 -0
  11. mcli/workflow/notebook/notebook_cmd.py +441 -0
  12. mcli/workflow/notebook/schema.py +402 -0
  13. mcli/workflow/notebook/validator.py +313 -0
  14. mcli/workflow/secrets/__init__.py +4 -0
  15. mcli/workflow/secrets/secrets_cmd.py +192 -0
  16. mcli/workflow/workflow.py +35 -5
  17. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/METADATA +86 -55
  18. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/RECORD +22 -34
  19. mcli/ml/features/political_features.py +0 -677
  20. mcli/ml/preprocessing/politician_trading_preprocessor.py +0 -570
  21. mcli/workflow/politician_trading/__init__.py +0 -4
  22. mcli/workflow/politician_trading/config.py +0 -134
  23. mcli/workflow/politician_trading/connectivity.py +0 -492
  24. mcli/workflow/politician_trading/data_sources.py +0 -654
  25. mcli/workflow/politician_trading/database.py +0 -412
  26. mcli/workflow/politician_trading/demo.py +0 -249
  27. mcli/workflow/politician_trading/models.py +0 -327
  28. mcli/workflow/politician_trading/monitoring.py +0 -413
  29. mcli/workflow/politician_trading/scrapers.py +0 -1074
  30. mcli/workflow/politician_trading/scrapers_california.py +0 -434
  31. mcli/workflow/politician_trading/scrapers_corporate_registry.py +0 -797
  32. mcli/workflow/politician_trading/scrapers_eu.py +0 -376
  33. mcli/workflow/politician_trading/scrapers_free_sources.py +0 -509
  34. mcli/workflow/politician_trading/scrapers_third_party.py +0 -373
  35. mcli/workflow/politician_trading/scrapers_uk.py +0 -378
  36. mcli/workflow/politician_trading/scrapers_us_states.py +0 -471
  37. mcli/workflow/politician_trading/seed_database.py +0 -520
  38. mcli/workflow/politician_trading/supabase_functions.py +0 -354
  39. mcli/workflow/politician_trading/workflow.py +0 -879
  40. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/WHEEL +0 -0
  41. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/entry_points.txt +0 -0
  42. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/licenses/LICENSE +0 -0
  43. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/top_level.txt +0 -0
mcli/app/commands_cmd.py CHANGED
@@ -172,38 +172,48 @@ def restore_command_state(hash_value):
172
172
  return True
173
173
 
174
174
 
175
- @click.group()
176
- def commands():
177
- """Manage and execute available commands."""
175
+ @click.group(name="workflow")
176
+ def workflow():
177
+ """Manage workflows - create, edit, import, export workflow commands."""
178
178
  pass
179
179
 
180
180
 
181
- @commands.command("list")
181
+ # For backward compatibility, keep commands as an alias
182
+ commands = workflow
183
+
184
+
185
+ @workflow.command("list")
182
186
  @click.option("--include-groups", is_flag=True, help="Include command groups in listing")
183
187
  @click.option("--daemon-only", is_flag=True, help="Show only daemon database commands")
184
188
  @click.option(
185
- "--custom-only", is_flag=True, help="Show only custom commands from ~/.mcli/commands/"
189
+ "--custom-only", is_flag=True, help="Show only custom commands from command directory"
186
190
  )
187
191
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
188
- def list_commands(include_groups: bool, daemon_only: bool, custom_only: bool, as_json: bool):
192
+ @click.option(
193
+ "--global", "-g", "is_global", is_flag=True, help="Use global commands (~/.mcli/commands/) instead of local (.mcli/commands/)"
194
+ )
195
+ def list_commands(include_groups: bool, daemon_only: bool, custom_only: bool, as_json: bool, is_global: bool):
189
196
  """
190
197
  List all available commands.
191
198
 
192
199
  By default, shows all discovered Click commands. Use flags to filter:
193
- - --custom-only: Show only custom commands from ~/.mcli/commands/
200
+ - --custom-only: Show only custom commands
194
201
  - --daemon-only: Show only daemon database commands
202
+ - --global/-g: Use global commands directory instead of local
195
203
 
196
204
  Examples:
197
- mcli commands list # Show all commands
198
- mcli commands list --custom-only # Show only custom commands
205
+ mcli commands list # Show all commands (local if in git repo)
206
+ mcli commands list --custom-only # Show only custom commands (local if in git repo)
207
+ mcli commands list --global # Show all global commands
208
+ mcli commands list --custom-only -g # Show only global custom commands
199
209
  mcli commands list --json # Output as JSON
200
210
  """
201
211
  from rich.table import Table
202
212
 
203
213
  try:
204
214
  if custom_only:
205
- # Show only custom commands from ~/.mcli/commands/
206
- manager = get_command_manager()
215
+ # Show only custom commands
216
+ manager = get_command_manager(global_mode=is_global)
207
217
  cmds = manager.load_all_commands()
208
218
 
209
219
  if not cmds:
@@ -232,7 +242,14 @@ def list_commands(include_groups: bool, daemon_only: bool, custom_only: bool, as
232
242
  )
233
243
 
234
244
  console.print(table)
235
- console.print(f"\n[dim]Commands directory: {manager.commands_dir}[/dim]")
245
+
246
+ # Show context information
247
+ scope = "global" if is_global or not manager.is_local else "local"
248
+ scope_color = "yellow" if scope == "local" else "cyan"
249
+ console.print(f"\n[dim]Scope: [{scope_color}]{scope}[/{scope_color}][/dim]")
250
+ if manager.is_local and manager.git_root:
251
+ console.print(f"[dim]Git repository: {manager.git_root}[/dim]")
252
+ console.print(f"[dim]Commands directory: {manager.commands_dir}[/dim]")
236
253
  console.print(f"[dim]Lockfile: {manager.lockfile_path}[/dim]")
237
254
 
238
255
  return 0
@@ -287,12 +304,19 @@ def list_commands(include_groups: bool, daemon_only: bool, custom_only: bool, as
287
304
  console.print(f"[red]Error: {e}[/red]")
288
305
 
289
306
 
290
- @commands.command("search")
307
+ @workflow.command("search")
291
308
  @click.argument("query")
292
309
  @click.option("--daemon-only", is_flag=True, help="Search only daemon database commands")
293
310
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
294
- def search_commands(query: str, daemon_only: bool, as_json: bool):
295
- """Search commands by name, description, or tags"""
311
+ @click.option(
312
+ "--global", "-g", "is_global", is_flag=True, help="Search global commands instead of local"
313
+ )
314
+ def search_commands(query: str, daemon_only: bool, as_json: bool, is_global: bool):
315
+ """
316
+ Search commands by name, description, or tags.
317
+
318
+ By default searches local commands (if in git repo), use --global/-g for global commands.
319
+ """
296
320
  try:
297
321
  if daemon_only:
298
322
  # Search only daemon database commands
@@ -358,7 +382,7 @@ def search_commands(query: str, daemon_only: bool, as_json: bool):
358
382
  console.print(f"[red]Error: {e}[/red]")
359
383
 
360
384
 
361
- @commands.command("execute")
385
+ @workflow.command("execute")
362
386
  @click.argument("command_name")
363
387
  @click.argument("args", nargs=-1)
364
388
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@@ -388,7 +412,7 @@ def execute_command(command_name: str, args: tuple, as_json: bool, timeout: Opti
388
412
  console.print(f"[red]Error: {e}[/red]")
389
413
 
390
414
 
391
- @commands.command("info")
415
+ @workflow.command("info")
392
416
  @click.argument("command_name")
393
417
  @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
394
418
  def command_info(command_name: str, as_json: bool):
@@ -676,9 +700,9 @@ logger = get_logger()
676
700
  pass
677
701
 
678
702
 
679
- @commands.command("add")
703
+ @workflow.command("add")
680
704
  @click.argument("command_name", required=True)
681
- @click.option("--group", "-g", help="Command group (defaults to 'workflow')", default="workflow")
705
+ @click.option("--group", help="Command group (defaults to 'workflow')", default="workflow")
682
706
  @click.option("--description", "-d", help="Description for the command", default="Custom command")
683
707
  @click.option(
684
708
  "--template",
@@ -699,7 +723,10 @@ logger = get_logger()
699
723
  type=click.Choice(["bash", "zsh", "fish", "sh"], case_sensitive=False),
700
724
  help="Shell type for shell commands (defaults to $SHELL)",
701
725
  )
702
- def add_command(command_name, group, description, template, language, shell):
726
+ @click.option(
727
+ "--global", "-g", "is_global", is_flag=True, help="Add to global commands (~/.mcli/commands/) instead of local (.mcli/commands/)"
728
+ )
729
+ def add_command(command_name, group, description, template, language, shell, is_global):
703
730
  """
704
731
  Generate a new portable custom command saved to ~/.mcli/commands/.
705
732
 
@@ -749,7 +776,7 @@ def add_command(command_name, group, description, template, language, shell):
749
776
  command_group = "workflow" # Default to workflow group
750
777
 
751
778
  # Get the command manager
752
- manager = get_command_manager()
779
+ manager = get_command_manager(global_mode=is_global)
753
780
 
754
781
  # Check if command already exists
755
782
  command_file = manager.commands_dir / f"{command_name}.json"
@@ -830,29 +857,45 @@ def add_command(command_name, group, description, template, language, shell):
830
857
  )
831
858
 
832
859
  lang_display = f"{language}" if language == "python" else f"{language} ({shell})"
833
- logger.info(f"Created portable custom command: {command_name} ({lang_display})")
860
+ scope = "global" if is_global or not manager.is_local else "local"
861
+ scope_display = f"[cyan]{scope}[/cyan]" if scope == "global" else f"[yellow]{scope}[/yellow]"
862
+
863
+ logger.info(f"Created portable custom command: {command_name} ({lang_display}) [{scope}]")
834
864
  console.print(
835
- f"[green]Created portable custom command: {command_name}[/green] [dim]({lang_display})[/dim]"
865
+ f"[green]Created portable custom command: {command_name}[/green] [dim]({lang_display}) [Scope: {scope_display}][/dim]"
836
866
  )
837
867
  console.print(f"[dim]Saved to: {saved_path}[/dim]")
868
+ if manager.is_local and manager.git_root:
869
+ console.print(f"[dim]Git repository: {manager.git_root}[/dim]")
838
870
  console.print(f"[dim]Group: {command_group}[/dim]")
839
871
  console.print(f"[dim]Execute with: mcli {command_group} {command_name}[/dim]")
840
872
  console.print("[dim]Command will be automatically loaded on next mcli startup[/dim]")
841
- console.print(
842
- f"[dim]You can share this command by copying {saved_path} to another machine's ~/.mcli/commands/ directory[/dim]"
843
- )
873
+
874
+ if scope == "global":
875
+ console.print(
876
+ f"[dim]You can share this command by copying {saved_path} to another machine's ~/.mcli/commands/ directory[/dim]"
877
+ )
878
+ else:
879
+ console.print(
880
+ f"[dim]This command is local to this git repository. Use --global/-g to create global commands.[/dim]"
881
+ )
844
882
 
845
883
  return 0
846
884
 
847
885
 
848
- @commands.command("remove")
886
+ @workflow.command("remove")
849
887
  @click.argument("command_name", required=True)
850
888
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
851
- def remove_command(command_name, yes):
889
+ @click.option(
890
+ "--global", "-g", "is_global", is_flag=True, help="Remove from global commands instead of local"
891
+ )
892
+ def remove_command(command_name, yes, is_global):
852
893
  """
853
- Remove a custom command from ~/.mcli/commands/.
894
+ Remove a custom command.
895
+
896
+ By default removes from local commands (if in git repo), use --global/-g for global commands.
854
897
  """
855
- manager = get_command_manager()
898
+ manager = get_command_manager(global_mode=is_global)
856
899
  command_file = manager.commands_dir / f"{command_name}.json"
857
900
 
858
901
  if not command_file.exists():
@@ -875,7 +918,7 @@ def remove_command(command_name, yes):
875
918
  return 1
876
919
 
877
920
 
878
- @commands.command("export")
921
+ @workflow.command("export")
879
922
  @click.argument("target", type=click.Path(), required=False)
880
923
  @click.option(
881
924
  "--script", "-s", is_flag=True, help="Export as Python script (requires command name)"
@@ -884,7 +927,10 @@ def remove_command(command_name, yes):
884
927
  "--standalone", is_flag=True, help="Make script standalone (add if __name__ == '__main__')"
885
928
  )
886
929
  @click.option("--output", "-o", type=click.Path(), help="Output file path")
887
- def export_commands(target, script, standalone, output):
930
+ @click.option(
931
+ "--global", "-g", "is_global", is_flag=True, help="Export from global commands instead of local"
932
+ )
933
+ def export_commands(target, script, standalone, output, is_global):
888
934
  """
889
935
  Export custom commands to JSON file or export a single command to Python script.
890
936
 
@@ -892,12 +938,13 @@ def export_commands(target, script, standalone, output):
892
938
  With --script/-s: Export a single command to Python script
893
939
 
894
940
  Examples:
895
- mcli commands export # Export all to commands-export.json
941
+ mcli commands export # Export all local commands
942
+ mcli commands export --global # Export all global commands
896
943
  mcli commands export my-export.json # Export all to specified file
897
944
  mcli commands export my-cmd -s # Export command to my-cmd.py
898
945
  mcli commands export my-cmd -s -o out.py --standalone
899
946
  """
900
- manager = get_command_manager()
947
+ manager = get_command_manager(global_mode=is_global)
901
948
 
902
949
  if script:
903
950
  # Export single command to Python script
@@ -972,12 +1019,12 @@ def export_commands(target, script, standalone, output):
972
1019
  return 1
973
1020
 
974
1021
 
975
- @commands.command("import")
1022
+ @workflow.command("import")
976
1023
  @click.argument("source", type=click.Path(exists=True), required=True)
977
1024
  @click.option("--script", "-s", is_flag=True, help="Import from Python script")
978
1025
  @click.option("--overwrite", is_flag=True, help="Overwrite existing commands")
979
1026
  @click.option("--name", "-n", help="Command name (for script import, defaults to script filename)")
980
- @click.option("--group", "-g", default="workflow", help="Command group (for script import)")
1027
+ @click.option("--group", default="workflow", help="Command group (for script import)")
981
1028
  @click.option("--description", "-d", help="Command description (for script import)")
982
1029
  @click.option(
983
1030
  "--interactive",
@@ -985,7 +1032,10 @@ def export_commands(target, script, standalone, output):
985
1032
  is_flag=True,
986
1033
  help="Open in $EDITOR for review/editing (for script import)",
987
1034
  )
988
- def import_commands(source, script, overwrite, name, group, description, interactive):
1035
+ @click.option(
1036
+ "--global", "-g", "is_global", is_flag=True, help="Import to global commands instead of local"
1037
+ )
1038
+ def import_commands(source, script, overwrite, name, group, description, interactive, is_global):
989
1039
  """
990
1040
  Import custom commands from JSON file or import a Python script as a command.
991
1041
 
@@ -993,13 +1043,14 @@ def import_commands(source, script, overwrite, name, group, description, interac
993
1043
  With --script/-s: Import a Python script as a command
994
1044
 
995
1045
  Examples:
996
- mcli commands import commands-export.json
1046
+ mcli commands import commands-export.json # Import to local (if in git repo)
1047
+ mcli commands import commands-export.json --global # Import to global
997
1048
  mcli commands import my_script.py -s
998
1049
  mcli commands import my_script.py -s --name custom-cmd --interactive
999
1050
  """
1000
1051
  import subprocess
1001
1052
 
1002
- manager = get_command_manager()
1053
+ manager = get_command_manager(global_mode=is_global)
1003
1054
  source_path = Path(source)
1004
1055
 
1005
1056
  if script:
@@ -1142,12 +1193,17 @@ def import_commands(source, script, overwrite, name, group, description, interac
1142
1193
  return 0
1143
1194
 
1144
1195
 
1145
- @commands.command("verify")
1146
- def verify_commands():
1196
+ @workflow.command("verify")
1197
+ @click.option(
1198
+ "--global", "-g", "is_global", is_flag=True, help="Verify global commands instead of local"
1199
+ )
1200
+ def verify_commands(is_global):
1147
1201
  """
1148
1202
  Verify that custom commands match the lockfile.
1203
+
1204
+ By default verifies local commands (if in git repo), use --global/-g for global commands.
1149
1205
  """
1150
- manager = get_command_manager()
1206
+ manager = get_command_manager(global_mode=is_global)
1151
1207
 
1152
1208
  # First, ensure lockfile is up to date
1153
1209
  manager.update_lockfile()
@@ -1180,12 +1236,17 @@ def verify_commands():
1180
1236
  return 1
1181
1237
 
1182
1238
 
1183
- @commands.command("update-lockfile")
1184
- def update_lockfile():
1239
+ @workflow.command("update-lockfile")
1240
+ @click.option(
1241
+ "--global", "-g", "is_global", is_flag=True, help="Update global lockfile instead of local"
1242
+ )
1243
+ def update_lockfile(is_global):
1185
1244
  """
1186
1245
  Update the commands lockfile with current state.
1246
+
1247
+ By default updates local lockfile (if in git repo), use --global/-g for global lockfile.
1187
1248
  """
1188
- manager = get_command_manager()
1249
+ manager = get_command_manager(global_mode=is_global)
1189
1250
 
1190
1251
  if manager.update_lockfile():
1191
1252
  console.print(f"[green]Updated lockfile: {manager.lockfile_path}[/green]")
@@ -1195,10 +1256,13 @@ def update_lockfile():
1195
1256
  return 1
1196
1257
 
1197
1258
 
1198
- @commands.command("edit")
1259
+ @workflow.command("edit")
1199
1260
  @click.argument("command_name")
1200
1261
  @click.option("--editor", "-e", help="Editor to use (defaults to $EDITOR)")
1201
- def edit_command(command_name, editor):
1262
+ @click.option(
1263
+ "--global", "-g", "is_global", is_flag=True, help="Edit global command instead of local"
1264
+ )
1265
+ def edit_command(command_name, editor, is_global):
1202
1266
  """
1203
1267
  Edit a command interactively using $EDITOR.
1204
1268
 
@@ -1206,12 +1270,13 @@ def edit_command(command_name, editor):
1206
1270
  allows you to make changes, and saves the updated version.
1207
1271
 
1208
1272
  Examples:
1209
- mcli commands edit my-command
1273
+ mcli commands edit my-command # Edit local command (if in git repo)
1274
+ mcli commands edit my-command --global # Edit global command
1210
1275
  mcli commands edit my-command --editor code
1211
1276
  """
1212
1277
  import subprocess
1213
1278
 
1214
- manager = get_command_manager()
1279
+ manager = get_command_manager(global_mode=is_global)
1215
1280
 
1216
1281
  # Load the command
1217
1282
  command_file = manager.commands_dir / f"{command_name}.json"
@@ -1294,7 +1359,7 @@ def edit_command(command_name, editor):
1294
1359
  # Moved from mcli.self for better organization
1295
1360
 
1296
1361
 
1297
- @commands.group("state")
1362
+ @workflow.group("state")
1298
1363
  def command_state():
1299
1364
  """Manage command state lockfile and history."""
1300
1365
  pass
@@ -1379,7 +1444,7 @@ def _get_store_path() -> Path:
1379
1444
  return DEFAULT_STORE_PATH
1380
1445
 
1381
1446
 
1382
- @commands.group("store")
1447
+ @workflow.group("store")
1383
1448
  def store():
1384
1449
  """Manage command store - sync ~/.mcli/commands/ to git"""
1385
1450
  pass
@@ -1466,10 +1531,19 @@ Last updated: {datetime.now().isoformat()}
1466
1531
  @store.command(name="push")
1467
1532
  @click.option("--message", "-m", help="Commit message")
1468
1533
  @click.option("--all", "-a", is_flag=True, help="Push all files (including backups)")
1469
- def push_commands(message, all):
1470
- """Push commands from ~/.mcli/commands/ to git store"""
1534
+ @click.option(
1535
+ "--global", "-g", "is_global", is_flag=True, help="Push global commands instead of local"
1536
+ )
1537
+ def push_commands(message, all, is_global):
1538
+ """
1539
+ Push commands to git store.
1540
+
1541
+ By default pushes local commands (if in git repo), use --global/-g for global commands.
1542
+ """
1471
1543
  try:
1472
1544
  store_path = _get_store_path()
1545
+ from mcli.lib.paths import get_custom_commands_dir
1546
+ COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
1473
1547
 
1474
1548
  # Copy commands to store
1475
1549
  info(f"Copying commands from {COMMANDS_PATH} to {store_path}...")
@@ -1521,10 +1595,19 @@ def push_commands(message, all):
1521
1595
 
1522
1596
  @store.command(name="pull")
1523
1597
  @click.option("--force", "-f", is_flag=True, help="Overwrite local commands without backup")
1524
- def pull_commands(force):
1525
- """Pull commands from git store to ~/.mcli/commands/"""
1598
+ @click.option(
1599
+ "--global", "-g", "is_global", is_flag=True, help="Pull to global commands instead of local"
1600
+ )
1601
+ def pull_commands(force, is_global):
1602
+ """
1603
+ Pull commands from git store.
1604
+
1605
+ By default pulls to local commands (if in git repo), use --global/-g for global commands.
1606
+ """
1526
1607
  try:
1527
1608
  store_path = _get_store_path()
1609
+ from mcli.lib.paths import get_custom_commands_dir
1610
+ COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
1528
1611
 
1529
1612
  # Pull from remote
1530
1613
  try:
@@ -1569,10 +1652,19 @@ def pull_commands(force):
1569
1652
 
1570
1653
  @store.command(name="sync")
1571
1654
  @click.option("--message", "-m", help="Commit message if pushing")
1572
- def sync_commands(message):
1573
- """Sync commands bidirectionally (pull then push if changes)"""
1655
+ @click.option(
1656
+ "--global", "-g", "is_global", is_flag=True, help="Sync global commands instead of local"
1657
+ )
1658
+ def sync_commands(message, is_global):
1659
+ """
1660
+ Sync commands bidirectionally (pull then push if changes).
1661
+
1662
+ By default syncs local commands (if in git repo), use --global/-g for global commands.
1663
+ """
1574
1664
  try:
1575
1665
  store_path = _get_store_path()
1666
+ from mcli.lib.paths import get_custom_commands_dir
1667
+ COMMANDS_PATH = get_custom_commands_dir(global_mode=is_global)
1576
1668
 
1577
1669
  # First pull
1578
1670
  info("Pulling latest changes...")
@@ -1769,7 +1861,7 @@ def show_command(command_name, store):
1769
1861
  # Moved from mcli.self for better organization
1770
1862
 
1771
1863
 
1772
- @commands.command("extract-workflow-commands")
1864
+ @workflow.command("extract-workflow-commands")
1773
1865
  @click.option(
1774
1866
  "--output", "-o", type=click.Path(), help="Output file (default: workflow-commands.json)"
1775
1867
  )
mcli/app/main.py CHANGED
@@ -326,14 +326,14 @@ class LazyGroup(click.Group):
326
326
 
327
327
  def _add_lazy_commands(app: click.Group):
328
328
  """Add command groups with lazy loading."""
329
- # Essential commands - load immediately for fast access
329
+ # Workflow management - load immediately for fast access (renamed from 'commands')
330
330
  try:
331
- from mcli.app.commands_cmd import commands
331
+ from mcli.app.commands_cmd import workflow
332
332
 
333
- app.add_command(commands, name="commands")
334
- logger.debug("Added commands group")
333
+ app.add_command(workflow, name="workflow")
334
+ logger.debug("Added workflow management group")
335
335
  except ImportError as e:
336
- logger.debug(f"Could not load commands group: {e}")
336
+ logger.debug(f"Could not load workflow management group: {e}")
337
337
 
338
338
  # Self management - load immediately as it's commonly used
339
339
  try:
@@ -344,35 +344,29 @@ def _add_lazy_commands(app: click.Group):
344
344
  except Exception as e:
345
345
  logger.debug(f"Could not load self commands: {e}")
346
346
 
347
- # Library utilities and secrets management
348
- try:
349
- from mcli.lib.lib import lib
350
-
351
- app.add_command(lib, name="lib")
352
- logger.debug("Added lib commands")
353
- except Exception as e:
354
- logger.debug(f"Could not load lib commands: {e}")
347
+ # Note: lib group removed - secrets moved to workflows
348
+ # Previous: mcli lib secrets -> Now: mcli workflows secrets
355
349
 
356
- # Add workflow with completion-aware lazy loading
350
+ # Add workflows group (renamed from 'workflow') with completion-aware lazy loading
357
351
  try:
358
352
  from mcli.app.completion_helpers import create_completion_aware_lazy_group
359
353
 
360
- workflow_group = create_completion_aware_lazy_group(
361
- "workflow",
362
- "mcli.workflow.workflow.workflow",
363
- "Workflow commands for automation, video processing, and daemon management",
354
+ workflows_group = create_completion_aware_lazy_group(
355
+ "workflows",
356
+ "mcli.workflow.workflow.workflows",
357
+ "Runnable workflows for automation, video processing, and daemon management",
364
358
  )
365
- app.add_command(workflow_group, name="workflow")
366
- logger.debug("Added completion-aware workflow group")
359
+ app.add_command(workflows_group, name="workflows")
360
+ logger.debug("Added completion-aware workflows group")
367
361
  except ImportError as e:
368
362
  logger.debug(f"Could not load completion helpers, using standard lazy group: {e}")
369
363
  # Fallback to standard lazy group
370
- workflow_group = LazyGroup(
371
- "workflow",
372
- "mcli.workflow.workflow.workflow",
373
- help="Workflow commands for automation, video processing, and daemon management",
364
+ workflows_group = LazyGroup(
365
+ "workflows",
366
+ "mcli.workflow.workflow.workflows",
367
+ help="Runnable workflows for automation, video processing, and daemon management",
374
368
  )
375
- app.add_command(workflow_group, name="workflow")
369
+ app.add_command(workflows_group, name="workflows")
376
370
 
377
371
  # Lazy load other heavy commands that are used less frequently
378
372
  # NOTE: chat and model commands have been removed
@@ -382,8 +376,8 @@ def _add_lazy_commands(app: click.Group):
382
376
  lazy_commands = {}
383
377
 
384
378
  for cmd_name, cmd_info in lazy_commands.items():
385
- # Skip workflow since we already added it with completion support
386
- if cmd_name == "workflow":
379
+ # Skip workflows since we already added it with completion support
380
+ if cmd_name == "workflows":
387
381
  continue
388
382
 
389
383
  if cmd_name in ["model"]:
@@ -19,7 +19,12 @@ from typing import Any, Dict, List, Optional
19
19
  import click
20
20
 
21
21
  from mcli.lib.logger.logger import get_logger, register_subprocess
22
- from mcli.lib.paths import get_custom_commands_dir
22
+ from mcli.lib.paths import (
23
+ get_custom_commands_dir,
24
+ get_lockfile_path,
25
+ is_git_repository,
26
+ get_git_root,
27
+ )
23
28
 
24
29
  logger = get_logger()
25
30
 
@@ -27,10 +32,22 @@ logger = get_logger()
27
32
  class CustomCommandManager:
28
33
  """Manages custom user commands stored in JSON format."""
29
34
 
30
- def __init__(self):
31
- self.commands_dir = get_custom_commands_dir()
35
+ def __init__(self, global_mode: bool = False):
36
+ """
37
+ Initialize the custom command manager.
38
+
39
+ Args:
40
+ global_mode: If True, use global commands directory (~/.mcli/commands/).
41
+ If False, use local directory (.mcli/commands/) when in a git repository.
42
+ """
43
+ self.global_mode = global_mode
44
+ self.commands_dir = get_custom_commands_dir(global_mode=global_mode)
32
45
  self.loaded_commands: Dict[str, Any] = {}
33
- self.lockfile_path = self.commands_dir / "commands.lock.json"
46
+ self.lockfile_path = get_lockfile_path(global_mode=global_mode)
47
+
48
+ # Store context information for display
49
+ self.is_local = not global_mode and is_git_repository()
50
+ self.git_root = get_git_root() if self.is_local else None
34
51
 
35
52
  def save_command(
36
53
  self,
@@ -107,15 +124,25 @@ class CustomCommandManager:
107
124
  """
108
125
  Load all custom commands from the commands directory.
109
126
 
127
+ Automatically filters out test commands (starting with 'test_' or 'test-')
128
+ unless MCLI_INCLUDE_TEST_COMMANDS=true is set.
129
+
110
130
  Returns:
111
131
  List of command data dictionaries
112
132
  """
113
133
  commands = []
134
+ include_test = os.environ.get('MCLI_INCLUDE_TEST_COMMANDS', 'false').lower() == 'true'
135
+
114
136
  for command_file in self.commands_dir.glob("*.json"):
115
137
  # Skip the lockfile
116
138
  if command_file.name == "commands.lock.json":
117
139
  continue
118
140
 
141
+ # Skip test commands unless explicitly included
142
+ if not include_test and command_file.stem.startswith(('test_', 'test-')):
143
+ logger.debug(f"Skipping test command: {command_file.name}")
144
+ continue
145
+
119
146
  command_data = self.load_command(command_file)
120
147
  if command_data:
121
148
  commands.append(command_data)
@@ -476,16 +503,39 @@ class CustomCommandManager:
476
503
  return results
477
504
 
478
505
 
479
- # Global instance
480
- _command_manager: Optional[CustomCommandManager] = None
506
+ # Global and local instances
507
+ _global_command_manager: Optional[CustomCommandManager] = None
508
+ _local_command_manager: Optional[CustomCommandManager] = None
509
+
510
+
511
+ def get_command_manager(global_mode: bool = False) -> CustomCommandManager:
512
+ """
513
+ Get the custom command manager instance.
481
514
 
515
+ Args:
516
+ global_mode: If True, return global manager. If False, return local manager (if in git repo).
482
517
 
483
- def get_command_manager() -> CustomCommandManager:
484
- """Get the global custom command manager instance."""
485
- global _command_manager
486
- if _command_manager is None:
487
- _command_manager = CustomCommandManager()
488
- return _command_manager
518
+ Returns:
519
+ CustomCommandManager instance for the appropriate scope
520
+ """
521
+ global _global_command_manager, _local_command_manager
522
+
523
+ if global_mode:
524
+ if _global_command_manager is None:
525
+ _global_command_manager = CustomCommandManager(global_mode=True)
526
+ return _global_command_manager
527
+ else:
528
+ # Use local manager if in git repository
529
+ if is_git_repository():
530
+ # Recreate local manager if git root changed (e.g., changed directory)
531
+ if _local_command_manager is None or _local_command_manager.git_root != get_git_root():
532
+ _local_command_manager = CustomCommandManager(global_mode=False)
533
+ return _local_command_manager
534
+ else:
535
+ # Fallback to global manager when not in a git repository
536
+ if _global_command_manager is None:
537
+ _global_command_manager = CustomCommandManager(global_mode=True)
538
+ return _global_command_manager
489
539
 
490
540
 
491
541
  def load_custom_commands(target_group: click.Group) -> int: