mcli-framework 7.1.0__py3-none-any.whl → 7.1.2__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 (94) hide show
  1. mcli/app/completion_cmd.py +59 -49
  2. mcli/app/completion_helpers.py +60 -138
  3. mcli/app/logs_cmd.py +46 -13
  4. mcli/app/main.py +17 -14
  5. mcli/app/model_cmd.py +19 -4
  6. mcli/chat/chat.py +3 -2
  7. mcli/lib/search/cached_vectorizer.py +1 -0
  8. mcli/lib/services/data_pipeline.py +12 -5
  9. mcli/lib/services/lsh_client.py +69 -58
  10. mcli/ml/api/app.py +28 -36
  11. mcli/ml/api/middleware.py +8 -16
  12. mcli/ml/api/routers/admin_router.py +3 -1
  13. mcli/ml/api/routers/auth_router.py +32 -56
  14. mcli/ml/api/routers/backtest_router.py +3 -1
  15. mcli/ml/api/routers/data_router.py +3 -1
  16. mcli/ml/api/routers/model_router.py +35 -74
  17. mcli/ml/api/routers/monitoring_router.py +3 -1
  18. mcli/ml/api/routers/portfolio_router.py +3 -1
  19. mcli/ml/api/routers/prediction_router.py +60 -65
  20. mcli/ml/api/routers/trade_router.py +6 -2
  21. mcli/ml/api/routers/websocket_router.py +12 -9
  22. mcli/ml/api/schemas.py +10 -2
  23. mcli/ml/auth/auth_manager.py +49 -114
  24. mcli/ml/auth/models.py +30 -15
  25. mcli/ml/auth/permissions.py +12 -19
  26. mcli/ml/backtesting/backtest_engine.py +134 -108
  27. mcli/ml/backtesting/performance_metrics.py +142 -108
  28. mcli/ml/cache.py +12 -18
  29. mcli/ml/cli/main.py +37 -23
  30. mcli/ml/config/settings.py +29 -12
  31. mcli/ml/dashboard/app.py +122 -130
  32. mcli/ml/dashboard/app_integrated.py +283 -152
  33. mcli/ml/dashboard/app_supabase.py +176 -108
  34. mcli/ml/dashboard/app_training.py +212 -206
  35. mcli/ml/dashboard/cli.py +14 -5
  36. mcli/ml/data_ingestion/api_connectors.py +51 -81
  37. mcli/ml/data_ingestion/data_pipeline.py +127 -125
  38. mcli/ml/data_ingestion/stream_processor.py +72 -80
  39. mcli/ml/database/migrations/env.py +3 -2
  40. mcli/ml/database/models.py +112 -79
  41. mcli/ml/database/session.py +6 -5
  42. mcli/ml/experimentation/ab_testing.py +149 -99
  43. mcli/ml/features/ensemble_features.py +9 -8
  44. mcli/ml/features/political_features.py +6 -5
  45. mcli/ml/features/recommendation_engine.py +15 -14
  46. mcli/ml/features/stock_features.py +7 -6
  47. mcli/ml/features/test_feature_engineering.py +8 -7
  48. mcli/ml/logging.py +10 -15
  49. mcli/ml/mlops/data_versioning.py +57 -64
  50. mcli/ml/mlops/experiment_tracker.py +49 -41
  51. mcli/ml/mlops/model_serving.py +59 -62
  52. mcli/ml/mlops/pipeline_orchestrator.py +203 -149
  53. mcli/ml/models/base_models.py +8 -7
  54. mcli/ml/models/ensemble_models.py +6 -5
  55. mcli/ml/models/recommendation_models.py +7 -6
  56. mcli/ml/models/test_models.py +18 -14
  57. mcli/ml/monitoring/drift_detection.py +95 -74
  58. mcli/ml/monitoring/metrics.py +10 -22
  59. mcli/ml/optimization/portfolio_optimizer.py +172 -132
  60. mcli/ml/predictions/prediction_engine.py +235 -0
  61. mcli/ml/preprocessing/data_cleaners.py +6 -5
  62. mcli/ml/preprocessing/feature_extractors.py +7 -6
  63. mcli/ml/preprocessing/ml_pipeline.py +3 -2
  64. mcli/ml/preprocessing/politician_trading_preprocessor.py +11 -10
  65. mcli/ml/preprocessing/test_preprocessing.py +4 -4
  66. mcli/ml/scripts/populate_sample_data.py +36 -16
  67. mcli/ml/tasks.py +82 -83
  68. mcli/ml/tests/test_integration.py +86 -76
  69. mcli/ml/tests/test_training_dashboard.py +169 -142
  70. mcli/mygroup/test_cmd.py +2 -1
  71. mcli/self/self_cmd.py +38 -18
  72. mcli/self/test_cmd.py +2 -1
  73. mcli/workflow/dashboard/dashboard_cmd.py +13 -6
  74. mcli/workflow/lsh_integration.py +46 -58
  75. mcli/workflow/politician_trading/commands.py +576 -427
  76. mcli/workflow/politician_trading/config.py +7 -7
  77. mcli/workflow/politician_trading/connectivity.py +35 -33
  78. mcli/workflow/politician_trading/data_sources.py +72 -71
  79. mcli/workflow/politician_trading/database.py +18 -16
  80. mcli/workflow/politician_trading/demo.py +4 -3
  81. mcli/workflow/politician_trading/models.py +5 -5
  82. mcli/workflow/politician_trading/monitoring.py +13 -13
  83. mcli/workflow/politician_trading/scrapers.py +332 -224
  84. mcli/workflow/politician_trading/scrapers_california.py +116 -94
  85. mcli/workflow/politician_trading/scrapers_eu.py +70 -71
  86. mcli/workflow/politician_trading/scrapers_uk.py +118 -90
  87. mcli/workflow/politician_trading/scrapers_us_states.py +125 -92
  88. mcli/workflow/politician_trading/workflow.py +98 -71
  89. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/METADATA +2 -2
  90. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/RECORD +94 -93
  91. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/WHEEL +0 -0
  92. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/entry_points.txt +0 -0
  93. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/licenses/LICENSE +0 -0
  94. {mcli_framework-7.1.0.dist-info → mcli_framework-7.1.2.dist-info}/top_level.txt +0 -0
@@ -6,8 +6,10 @@ for bash, zsh, and fish shells.
6
6
  """
7
7
 
8
8
  import os
9
- import click
10
9
  from pathlib import Path
10
+
11
+ import click
12
+
11
13
  from mcli.lib.ui.styling import success
12
14
 
13
15
 
@@ -22,29 +24,29 @@ def completion():
22
24
  def bash_completion(ctx):
23
25
  """Generate bash completion script"""
24
26
  from click.shell_completion import BashComplete
25
-
27
+
26
28
  # Get the root CLI app
27
29
  app = ctx.find_root().command
28
30
  complete = BashComplete(app, {}, "mcli", "complete")
29
31
  script = complete.source()
30
-
32
+
31
33
  click.echo("# Bash completion script for MCLI")
32
34
  click.echo("# Add this to your ~/.bashrc or ~/.bash_profile:")
33
35
  click.echo()
34
36
  click.echo(script)
35
37
 
36
38
 
37
- @completion.command(name="zsh")
39
+ @completion.command(name="zsh")
38
40
  @click.pass_context
39
41
  def zsh_completion(ctx):
40
42
  """Generate zsh completion script"""
41
43
  from click.shell_completion import ZshComplete
42
-
44
+
43
45
  # Get the root CLI app
44
46
  app = ctx.find_root().command
45
47
  complete = ZshComplete(app, {}, "mcli", "complete")
46
48
  script = complete.source()
47
-
49
+
48
50
  click.echo("# Zsh completion script for MCLI")
49
51
  click.echo("# Add this to your ~/.zshrc:")
50
52
  click.echo()
@@ -56,12 +58,12 @@ def zsh_completion(ctx):
56
58
  def fish_completion(ctx):
57
59
  """Generate fish completion script"""
58
60
  from click.shell_completion import FishComplete
59
-
61
+
60
62
  # Get the root CLI app
61
63
  app = ctx.find_root().command
62
64
  complete = FishComplete(app, {}, "mcli", "complete")
63
65
  script = complete.source()
64
-
66
+
65
67
  click.echo("# Fish completion script for MCLI")
66
68
  click.echo("# Add this to ~/.config/fish/completions/mcli.fish:")
67
69
  click.echo()
@@ -69,45 +71,49 @@ def fish_completion(ctx):
69
71
 
70
72
 
71
73
  @completion.command(name="install")
72
- @click.option("--shell", type=click.Choice(['bash', 'zsh', 'fish']),
73
- help="Shell to install for (auto-detected if not specified)")
74
+ @click.option(
75
+ "--shell",
76
+ type=click.Choice(["bash", "zsh", "fish"]),
77
+ help="Shell to install for (auto-detected if not specified)",
78
+ )
74
79
  @click.pass_context
75
80
  def install_completion(ctx, shell):
76
81
  """Install shell completion for the current user"""
77
82
  import subprocess
78
-
83
+
79
84
  # Auto-detect shell if not specified
80
85
  if not shell:
81
- shell_path = os.environ.get('SHELL', '')
82
- if 'bash' in shell_path:
83
- shell = 'bash'
84
- elif 'zsh' in shell_path:
85
- shell = 'zsh'
86
- elif 'fish' in shell_path:
87
- shell = 'fish'
86
+ shell_path = os.environ.get("SHELL", "")
87
+ if "bash" in shell_path:
88
+ shell = "bash"
89
+ elif "zsh" in shell_path:
90
+ shell = "zsh"
91
+ elif "fish" in shell_path:
92
+ shell = "fish"
88
93
  else:
89
94
  click.echo("āŒ Could not auto-detect shell. Please specify --shell")
90
95
  return
91
-
96
+
92
97
  # Get the root CLI app
93
98
  app = ctx.find_root().command
94
-
99
+
95
100
  try:
96
- if shell == 'bash':
101
+ if shell == "bash":
97
102
  from click.shell_completion import BashComplete
103
+
98
104
  complete = BashComplete(app, {}, "mcli", "complete")
99
105
  script = complete.source()
100
-
106
+
101
107
  # Install to bash completion directory
102
108
  bash_completion_dir = Path.home() / ".bash_completion.d"
103
109
  bash_completion_dir.mkdir(exist_ok=True)
104
110
  completion_file = bash_completion_dir / "mcli"
105
111
  completion_file.write_text(script)
106
-
112
+
107
113
  # Add sourcing to .bashrc if needed
108
114
  bashrc = Path.home() / ".bashrc"
109
115
  source_line = f"[ -f {completion_file} ] && source {completion_file}"
110
-
116
+
111
117
  if bashrc.exists():
112
118
  content = bashrc.read_text()
113
119
  if source_line not in content:
@@ -120,50 +126,54 @@ def install_completion(ctx, shell):
120
126
  click.echo(f"āœ… Completion installed to {completion_file}")
121
127
  click.echo("šŸ’” Add this to your ~/.bashrc:")
122
128
  click.echo(source_line)
123
-
124
- elif shell == 'zsh':
125
- from click.shell_completion import ZshComplete
129
+
130
+ elif shell == "zsh":
131
+ from click.shell_completion import ZshComplete
132
+
126
133
  complete = ZshComplete(app, {}, "mcli", "complete")
127
134
  script = complete.source()
128
-
135
+
129
136
  # Install to zsh completion directory
130
137
  zsh_completion_dir = Path.home() / ".config" / "zsh" / "completions"
131
138
  zsh_completion_dir.mkdir(parents=True, exist_ok=True)
132
139
  completion_file = zsh_completion_dir / "_mcli"
133
140
  completion_file.write_text(script)
134
-
141
+
135
142
  # Add to fpath in .zshrc if needed
136
143
  zshrc = Path.home() / ".zshrc"
137
144
  fpath_line = f'fpath=("{zsh_completion_dir}" $fpath)'
138
-
145
+
139
146
  if zshrc.exists():
140
147
  content = zshrc.read_text()
141
148
  if str(zsh_completion_dir) not in content:
142
149
  with zshrc.open("a") as f:
143
- f.write(f"\n# MCLI completion\n{fpath_line}\nautoload -U compinit && compinit\n")
144
- click.echo("āœ… Added completion to ~/.zshrc")
150
+ f.write(
151
+ f"\n# MCLI completion\n{fpath_line}\nautoload -U compinit && compinit\n"
152
+ )
153
+ click.echo("āœ… Added completion to ~/.zshrc")
145
154
  else:
146
155
  click.echo("ā„¹ļø Completion already configured in ~/.zshrc")
147
156
  else:
148
157
  click.echo(f"āœ… Completion installed to {completion_file}")
149
158
  click.echo("šŸ’” Add this to your ~/.zshrc:")
150
159
  click.echo(f"{fpath_line}\nautoload -U compinit && compinit")
151
-
152
- elif shell == 'fish':
160
+
161
+ elif shell == "fish":
153
162
  from click.shell_completion import FishComplete
163
+
154
164
  complete = FishComplete(app, {}, "mcli", "complete")
155
165
  script = complete.source()
156
-
166
+
157
167
  # Install to fish completion directory
158
168
  fish_completion_dir = Path.home() / ".config" / "fish" / "completions"
159
169
  fish_completion_dir.mkdir(parents=True, exist_ok=True)
160
170
  completion_file = fish_completion_dir / "mcli.fish"
161
171
  completion_file.write_text(script)
162
172
  click.echo(f"āœ… Completion installed to {completion_file}")
163
-
173
+
164
174
  click.echo(f"šŸŽ‰ Shell completion for {shell} installed successfully!")
165
175
  click.echo("šŸ’” Restart your shell or source your profile to enable completions")
166
-
176
+
167
177
  except Exception as e:
168
178
  click.echo(f"āŒ Failed to install completion: {e}")
169
179
 
@@ -171,39 +181,39 @@ def install_completion(ctx, shell):
171
181
  @completion.command(name="status")
172
182
  def completion_status():
173
183
  """Check current shell completion status"""
174
- current_shell = os.environ.get('SHELL', 'unknown')
175
- shell_name = Path(current_shell).name if current_shell != 'unknown' else 'unknown'
176
-
184
+ current_shell = os.environ.get("SHELL", "unknown")
185
+ shell_name = Path(current_shell).name if current_shell != "unknown" else "unknown"
186
+
177
187
  click.echo(f"🐚 Current shell: {shell_name} ({current_shell})")
178
188
  click.echo()
179
-
189
+
180
190
  # Check for existing completions
181
191
  completions_found = []
182
-
192
+
183
193
  # Check bash
184
194
  bash_completion = Path.home() / ".bash_completion.d" / "mcli"
185
195
  if bash_completion.exists():
186
196
  completions_found.append(f"āœ… Bash completion: {bash_completion}")
187
197
  else:
188
198
  completions_found.append("āŒ Bash completion: Not installed")
189
-
199
+
190
200
  # Check zsh
191
- zsh_completion = Path.home() / ".config" / "zsh" / "completions" / "_mcli"
201
+ zsh_completion = Path.home() / ".config" / "zsh" / "completions" / "_mcli"
192
202
  if zsh_completion.exists():
193
203
  completions_found.append(f"āœ… Zsh completion: {zsh_completion}")
194
204
  else:
195
205
  completions_found.append("āŒ Zsh completion: Not installed")
196
-
206
+
197
207
  # Check fish
198
208
  fish_completion = Path.home() / ".config" / "fish" / "completions" / "mcli.fish"
199
209
  if fish_completion.exists():
200
210
  completions_found.append(f"āœ… Fish completion: {fish_completion}")
201
211
  else:
202
212
  completions_found.append("āŒ Fish completion: Not installed")
203
-
213
+
204
214
  for status in completions_found:
205
215
  click.echo(status)
206
-
216
+
207
217
  click.echo()
208
218
  click.echo("šŸ’” To install completion for your shell:")
209
219
  click.echo(" mcli completion install")
@@ -212,5 +222,5 @@ def completion_status():
212
222
  click.echo(f" mcli completion {shell_name}")
213
223
 
214
224
 
215
- # Export the CLI group for registration
216
- cli = completion
225
+ # Export the CLI group for registration
226
+ cli = completion
@@ -2,7 +2,8 @@
2
2
  Completion helpers for MCLI that provide tab completion without loading heavy modules
3
3
  """
4
4
 
5
- from typing import List, Dict, Any
5
+ from typing import Any, Dict, List
6
+
6
7
  import click
7
8
  from click.shell_completion import CompletionItem
8
9
 
@@ -11,12 +12,12 @@ LAZY_COMMAND_COMPLETIONS = {
11
12
  "workflow": {
12
13
  "subcommands": [
13
14
  "api-daemon",
14
- "daemon",
15
+ "daemon",
15
16
  "file",
16
17
  "politician-trading",
17
18
  "scheduler",
18
19
  "sync",
19
- "videos"
20
+ "videos",
20
21
  ],
21
22
  "politician-trading": {
22
23
  "subcommands": [
@@ -24,7 +25,7 @@ LAZY_COMMAND_COMPLETIONS = {
24
25
  "cron-job",
25
26
  "data-sources",
26
27
  "disclosures",
27
- "health",
28
+ "health",
28
29
  "jobs",
29
30
  "monitor",
30
31
  "politicians",
@@ -34,105 +35,50 @@ LAZY_COMMAND_COMPLETIONS = {
34
35
  "stats",
35
36
  "status",
36
37
  "test-workflow",
37
- "verify"
38
+ "verify",
38
39
  ],
39
- "cron-job": {
40
- "options": ["--create", "--test"]
41
- },
40
+ "cron-job": {"options": ["--create", "--test"]},
42
41
  "setup": {
43
42
  "options": ["--create-tables", "--verify", "--generate-schema", "--output-dir"]
44
43
  },
45
- "run": {
46
- "options": ["--full", "--us-only", "--eu-only"]
47
- },
48
- "connectivity": {
49
- "options": ["--json", "--continuous", "--interval", "--duration"]
50
- },
51
- "health": {
52
- "options": ["--json"]
53
- },
54
- "monitor": {
55
- "options": ["--interval", "--count"]
56
- },
57
- "status": {
58
- "options": ["--json"]
59
- },
60
- "stats": {
61
- "options": ["--json"]
62
- },
63
- "test-workflow": {
64
- "options": ["--verbose", "--validate-writes"]
65
- },
66
- "schema": {
67
- "options": ["--show-location", "--generate", "--output-dir"]
68
- },
69
- "data-sources": {
70
- "options": ["--json"]
71
- },
72
- "jobs": {
73
- "options": ["--json", "--limit"]
74
- },
44
+ "run": {"options": ["--full", "--us-only", "--eu-only"]},
45
+ "connectivity": {"options": ["--json", "--continuous", "--interval", "--duration"]},
46
+ "health": {"options": ["--json"]},
47
+ "monitor": {"options": ["--interval", "--count"]},
48
+ "status": {"options": ["--json"]},
49
+ "stats": {"options": ["--json"]},
50
+ "test-workflow": {"options": ["--verbose", "--validate-writes"]},
51
+ "schema": {"options": ["--show-location", "--generate", "--output-dir"]},
52
+ "data-sources": {"options": ["--json"]},
53
+ "jobs": {"options": ["--json", "--limit"]},
75
54
  "politicians": {
76
55
  "options": ["--json", "--limit", "--role", "--party", "--state", "--search"]
77
56
  },
78
57
  "disclosures": {
79
- "options": ["--json", "--limit", "--politician", "--asset", "--transaction-type", "--amount-min", "--amount-max", "--days", "--details"]
58
+ "options": [
59
+ "--json",
60
+ "--limit",
61
+ "--politician",
62
+ "--asset",
63
+ "--transaction-type",
64
+ "--amount-min",
65
+ "--amount-max",
66
+ "--days",
67
+ "--details",
68
+ ]
80
69
  },
81
- "verify": {
82
- "options": ["--json"]
83
- }
70
+ "verify": {"options": ["--json"]},
84
71
  },
85
72
  "scheduler": {
86
- "subcommands": [
87
- "add",
88
- "cancel",
89
- "list",
90
- "monitor",
91
- "remove",
92
- "start",
93
- "status",
94
- "stop"
95
- ]
96
- },
97
- "daemon": {
98
- "subcommands": [
99
- "start",
100
- "stop",
101
- "status",
102
- "logs"
103
- ]
73
+ "subcommands": ["add", "cancel", "list", "monitor", "remove", "start", "status", "stop"]
104
74
  },
105
- "api-daemon": {
106
- "subcommands": [
107
- "start",
108
- "stop",
109
- "status",
110
- "logs"
111
- ]
112
- },
113
- "videos": {
114
- "subcommands": [
115
- "remove-overlay",
116
- "extract-frames",
117
- "create-video"
118
- ]
119
- },
120
- "sync": {
121
- "subcommands": [
122
- "status",
123
- "sync"
124
- ]
125
- },
126
- "file": {
127
- "subcommands": [
128
- "search",
129
- "organize"
130
- ]
131
- }
132
- },
133
- "chat": {
134
- "options": ["--model", "--system", "--temperature", "--max-tokens", "--stream"]
75
+ "daemon": {"subcommands": ["start", "stop", "status", "logs"]},
76
+ "api-daemon": {"subcommands": ["start", "stop", "status", "logs"]},
77
+ "videos": {"subcommands": ["remove-overlay", "extract-frames", "create-video"]},
78
+ "sync": {"subcommands": ["status", "sync"]},
79
+ "file": {"subcommands": ["search", "organize"]},
135
80
  },
81
+ "chat": {"options": ["--model", "--system", "--temperature", "--max-tokens", "--stream"]},
136
82
  "model": {
137
83
  "subcommands": [
138
84
  "download",
@@ -142,79 +88,51 @@ LAZY_COMMAND_COMPLETIONS = {
142
88
  "pull",
143
89
  "delete",
144
90
  "recommend",
145
- "status"
146
- ]
147
- },
148
- "cron-test": {
149
- "options": ["--quick", "--cleanup", "--verbose"]
150
- },
151
- "visual": {
152
- "subcommands": [
153
- "demo",
154
- "spinner-test"
155
- ]
156
- },
157
- "redis": {
158
- "subcommands": [
159
- "start",
160
- "stop",
161
91
  "status",
162
- "flush"
163
- ]
164
- },
165
- "logs": {
166
- "subcommands": [
167
- "tail",
168
- "view",
169
- "clear"
170
92
  ]
171
93
  },
94
+ "cron-test": {"options": ["--quick", "--cleanup", "--verbose"]},
95
+ "visual": {"subcommands": ["demo", "spinner-test"]},
96
+ "redis": {"subcommands": ["start", "stop", "status", "flush"]},
97
+ "logs": {"subcommands": ["tail", "view", "clear"]},
172
98
  "completion": {
173
- "subcommands": [
174
- "bash",
175
- "zsh",
176
- "fish",
177
- "install",
178
- "status"
179
- ],
180
- "install": {
181
- "options": ["--shell"]
182
- }
183
- }
99
+ "subcommands": ["bash", "zsh", "fish", "install", "status"],
100
+ "install": {"options": ["--shell"]},
101
+ },
184
102
  }
185
103
 
186
104
 
187
105
  def get_completion_items(cmd_path: List[str], incomplete: str = "") -> List[CompletionItem]:
188
106
  """Get completion items for a given command path without loading modules"""
189
107
  items = []
190
-
108
+
191
109
  # Navigate to the completion data for this path
192
110
  current_data = LAZY_COMMAND_COMPLETIONS
193
-
111
+
194
112
  for part in cmd_path:
195
113
  if part in current_data:
196
114
  current_data = current_data[part]
197
115
  else:
198
116
  return items # No completion data available
199
-
117
+
200
118
  # Add subcommands
201
119
  if "subcommands" in current_data:
202
120
  for subcommand in current_data["subcommands"]:
203
121
  if subcommand.startswith(incomplete):
204
122
  items.append(CompletionItem(subcommand))
205
-
206
- # Add options
123
+
124
+ # Add options
207
125
  if "options" in current_data:
208
126
  for option in current_data["options"]:
209
127
  if option.startswith(incomplete):
210
128
  items.append(CompletionItem(option))
211
-
129
+
212
130
  return items
213
131
 
214
132
 
215
133
  class CompletionAwareLazyGroup(click.Group):
216
134
  """A Click group that provides completion without loading modules"""
217
-
135
+
218
136
  def __init__(self, name, import_path, *args, **kwargs):
219
137
  self.import_path = import_path
220
138
  self._loaded_group = None
@@ -225,6 +143,7 @@ class CompletionAwareLazyGroup(click.Group):
225
143
  if self._loaded_group is None:
226
144
  try:
227
145
  import importlib
146
+
228
147
  module_path, attr_name = self.import_path.rsplit(".", 1)
229
148
  module = importlib.import_module(module_path)
230
149
  self._loaded_group = getattr(module, attr_name)
@@ -232,6 +151,7 @@ class CompletionAwareLazyGroup(click.Group):
232
151
  # Return a dummy group that shows an error
233
152
  def error_callback():
234
153
  click.echo(f"Error: Command group {self.name} is not available")
154
+
235
155
  self._loaded_group = click.Group(self.name, callback=error_callback)
236
156
  return self._loaded_group
237
157
 
@@ -252,13 +172,13 @@ class CompletionAwareLazyGroup(click.Group):
252
172
  data = LAZY_COMMAND_COMPLETIONS[self.name]
253
173
  if "subcommands" in data:
254
174
  return sorted(data["subcommands"])
255
-
175
+
256
176
  # Try to get from static completion data for other commands
257
177
  if self.name in LAZY_COMMAND_COMPLETIONS:
258
178
  data = LAZY_COMMAND_COMPLETIONS[self.name]
259
179
  if "subcommands" in data:
260
180
  return sorted(data["subcommands"])
261
-
181
+
262
182
  # Fallback to loading the actual group
263
183
  group = self._load_group()
264
184
  return group.list_commands(ctx)
@@ -274,10 +194,10 @@ class CompletionAwareLazyGroup(click.Group):
274
194
  if subcommand.startswith(incomplete):
275
195
  items.append(CompletionItem(subcommand))
276
196
  return items
277
-
197
+
278
198
  # Fallback to loading the actual group
279
199
  group = self._load_group()
280
- if hasattr(group, 'shell_complete'):
200
+ if hasattr(group, "shell_complete"):
281
201
  return group.shell_complete(ctx, incomplete)
282
202
  return []
283
203
 
@@ -287,6 +207,8 @@ class CompletionAwareLazyGroup(click.Group):
287
207
  return group.get_params(ctx)
288
208
 
289
209
 
290
- def create_completion_aware_lazy_group(name: str, import_path: str, help_text: str = None) -> CompletionAwareLazyGroup:
210
+ def create_completion_aware_lazy_group(
211
+ name: str, import_path: str, help_text: str = None
212
+ ) -> CompletionAwareLazyGroup:
291
213
  """Create a completion-aware lazy group"""
292
- return CompletionAwareLazyGroup(name, import_path, help=help_text)
214
+ return CompletionAwareLazyGroup(name, import_path, help=help_text)
mcli/app/logs_cmd.py CHANGED
@@ -120,8 +120,18 @@ def list_logs(date: Optional[str]):
120
120
  @click.argument("log_type", type=click.Choice(["main", "trace", "system"]))
121
121
  @click.option("--lines", "-n", type=int, default=20, help="Number of lines to show (default: 20)")
122
122
  @click.option("--date", "-d", help="Date for log file (YYYYMMDD format, default: today)")
123
- def tail_logs(log_type: str, lines: int, date: Optional[str]):
124
- """Show the last N lines of a specific log file"""
123
+ @click.option(
124
+ "--follow",
125
+ "-f",
126
+ is_flag=True,
127
+ help="Follow log output in real-time (like tail -f)",
128
+ )
129
+ def tail_logs(log_type: str, lines: int, date: Optional[str], follow: bool):
130
+ """Show the last N lines of a specific log file
131
+
132
+ By default, shows the last N lines and exits. Use --follow/-f to
133
+ continuously monitor the log file for new entries (similar to tail -f).
134
+ """
125
135
  logs_dir = get_logs_dir()
126
136
 
127
137
  # Note: get_logs_dir() creates the directory automatically
@@ -144,17 +154,40 @@ def tail_logs(log_type: str, lines: int, date: Optional[str]):
144
154
  return
145
155
 
146
156
  try:
147
- # Read last N lines
148
- with open(log_file, "r") as f:
149
- all_lines = f.readlines()
150
- tail_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
151
-
152
- # Display with formatting
153
- console.print(f"\nšŸ“‹ **Last {len(tail_lines)} lines from {log_file.name}**\n", style="cyan")
154
-
155
- for line in tail_lines:
156
- formatted_line = _format_log_line(line.rstrip())
157
- console.print(formatted_line)
157
+ if follow:
158
+ # Follow mode: continuously stream new lines
159
+ console.print(f"\nšŸ“” **Following {log_file.name}** (last {lines} lines)", style="cyan")
160
+ console.print("Press Ctrl+C to stop\n")
161
+
162
+ # Use tail -f for real-time following
163
+ cmd = ["tail", f"-n{lines}", "-f", str(log_file)]
164
+ process = subprocess.Popen(
165
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
166
+ )
167
+
168
+ try:
169
+ for line in iter(process.stdout.readline, ""):
170
+ if line:
171
+ formatted_line = _format_log_line(line.rstrip())
172
+ console.print(formatted_line)
173
+ except KeyboardInterrupt:
174
+ process.terminate()
175
+ console.print("\nšŸ‘‹ Log following stopped", style="cyan")
176
+ else:
177
+ # Standard mode: just show last N lines
178
+ # Read last N lines
179
+ with open(log_file, "r") as f:
180
+ all_lines = f.readlines()
181
+ tail_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
182
+
183
+ # Display with formatting
184
+ console.print(
185
+ f"\nšŸ“‹ **Last {len(tail_lines)} lines from {log_file.name}**\n", style="cyan"
186
+ )
187
+
188
+ for line in tail_lines:
189
+ formatted_line = _format_log_line(line.rstrip())
190
+ console.print(formatted_line)
158
191
 
159
192
  except Exception as e:
160
193
  console.print(f"āŒ Error reading log file: {e}", style="red")