mcli-framework 7.0.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 (186) hide show
  1. mcli/app/chat_cmd.py +42 -0
  2. mcli/app/commands_cmd.py +226 -0
  3. mcli/app/completion_cmd.py +216 -0
  4. mcli/app/completion_helpers.py +288 -0
  5. mcli/app/cron_test_cmd.py +697 -0
  6. mcli/app/logs_cmd.py +419 -0
  7. mcli/app/main.py +492 -0
  8. mcli/app/model/model.py +1060 -0
  9. mcli/app/model_cmd.py +227 -0
  10. mcli/app/redis_cmd.py +269 -0
  11. mcli/app/video/video.py +1114 -0
  12. mcli/app/visual_cmd.py +303 -0
  13. mcli/chat/chat.py +2409 -0
  14. mcli/chat/command_rag.py +514 -0
  15. mcli/chat/enhanced_chat.py +652 -0
  16. mcli/chat/system_controller.py +1010 -0
  17. mcli/chat/system_integration.py +1016 -0
  18. mcli/cli.py +25 -0
  19. mcli/config.toml +20 -0
  20. mcli/lib/api/api.py +586 -0
  21. mcli/lib/api/daemon_client.py +203 -0
  22. mcli/lib/api/daemon_client_local.py +44 -0
  23. mcli/lib/api/daemon_decorator.py +217 -0
  24. mcli/lib/api/mcli_decorators.py +1032 -0
  25. mcli/lib/auth/auth.py +85 -0
  26. mcli/lib/auth/aws_manager.py +85 -0
  27. mcli/lib/auth/azure_manager.py +91 -0
  28. mcli/lib/auth/credential_manager.py +192 -0
  29. mcli/lib/auth/gcp_manager.py +93 -0
  30. mcli/lib/auth/key_manager.py +117 -0
  31. mcli/lib/auth/mcli_manager.py +93 -0
  32. mcli/lib/auth/token_manager.py +75 -0
  33. mcli/lib/auth/token_util.py +1011 -0
  34. mcli/lib/config/config.py +47 -0
  35. mcli/lib/discovery/__init__.py +1 -0
  36. mcli/lib/discovery/command_discovery.py +274 -0
  37. mcli/lib/erd/erd.py +1345 -0
  38. mcli/lib/erd/generate_graph.py +453 -0
  39. mcli/lib/files/files.py +76 -0
  40. mcli/lib/fs/fs.py +109 -0
  41. mcli/lib/lib.py +29 -0
  42. mcli/lib/logger/logger.py +611 -0
  43. mcli/lib/performance/optimizer.py +409 -0
  44. mcli/lib/performance/rust_bridge.py +502 -0
  45. mcli/lib/performance/uvloop_config.py +154 -0
  46. mcli/lib/pickles/pickles.py +50 -0
  47. mcli/lib/search/cached_vectorizer.py +479 -0
  48. mcli/lib/services/data_pipeline.py +460 -0
  49. mcli/lib/services/lsh_client.py +441 -0
  50. mcli/lib/services/redis_service.py +387 -0
  51. mcli/lib/shell/shell.py +137 -0
  52. mcli/lib/toml/toml.py +33 -0
  53. mcli/lib/ui/styling.py +47 -0
  54. mcli/lib/ui/visual_effects.py +634 -0
  55. mcli/lib/watcher/watcher.py +185 -0
  56. mcli/ml/api/app.py +215 -0
  57. mcli/ml/api/middleware.py +224 -0
  58. mcli/ml/api/routers/admin_router.py +12 -0
  59. mcli/ml/api/routers/auth_router.py +244 -0
  60. mcli/ml/api/routers/backtest_router.py +12 -0
  61. mcli/ml/api/routers/data_router.py +12 -0
  62. mcli/ml/api/routers/model_router.py +302 -0
  63. mcli/ml/api/routers/monitoring_router.py +12 -0
  64. mcli/ml/api/routers/portfolio_router.py +12 -0
  65. mcli/ml/api/routers/prediction_router.py +267 -0
  66. mcli/ml/api/routers/trade_router.py +12 -0
  67. mcli/ml/api/routers/websocket_router.py +76 -0
  68. mcli/ml/api/schemas.py +64 -0
  69. mcli/ml/auth/auth_manager.py +425 -0
  70. mcli/ml/auth/models.py +154 -0
  71. mcli/ml/auth/permissions.py +302 -0
  72. mcli/ml/backtesting/backtest_engine.py +502 -0
  73. mcli/ml/backtesting/performance_metrics.py +393 -0
  74. mcli/ml/cache.py +400 -0
  75. mcli/ml/cli/main.py +398 -0
  76. mcli/ml/config/settings.py +394 -0
  77. mcli/ml/configs/dvc_config.py +230 -0
  78. mcli/ml/configs/mlflow_config.py +131 -0
  79. mcli/ml/configs/mlops_manager.py +293 -0
  80. mcli/ml/dashboard/app.py +532 -0
  81. mcli/ml/dashboard/app_integrated.py +738 -0
  82. mcli/ml/dashboard/app_supabase.py +560 -0
  83. mcli/ml/dashboard/app_training.py +615 -0
  84. mcli/ml/dashboard/cli.py +51 -0
  85. mcli/ml/data_ingestion/api_connectors.py +501 -0
  86. mcli/ml/data_ingestion/data_pipeline.py +567 -0
  87. mcli/ml/data_ingestion/stream_processor.py +512 -0
  88. mcli/ml/database/migrations/env.py +94 -0
  89. mcli/ml/database/models.py +667 -0
  90. mcli/ml/database/session.py +200 -0
  91. mcli/ml/experimentation/ab_testing.py +845 -0
  92. mcli/ml/features/ensemble_features.py +607 -0
  93. mcli/ml/features/political_features.py +676 -0
  94. mcli/ml/features/recommendation_engine.py +809 -0
  95. mcli/ml/features/stock_features.py +573 -0
  96. mcli/ml/features/test_feature_engineering.py +346 -0
  97. mcli/ml/logging.py +85 -0
  98. mcli/ml/mlops/data_versioning.py +518 -0
  99. mcli/ml/mlops/experiment_tracker.py +377 -0
  100. mcli/ml/mlops/model_serving.py +481 -0
  101. mcli/ml/mlops/pipeline_orchestrator.py +614 -0
  102. mcli/ml/models/base_models.py +324 -0
  103. mcli/ml/models/ensemble_models.py +675 -0
  104. mcli/ml/models/recommendation_models.py +474 -0
  105. mcli/ml/models/test_models.py +487 -0
  106. mcli/ml/monitoring/drift_detection.py +676 -0
  107. mcli/ml/monitoring/metrics.py +45 -0
  108. mcli/ml/optimization/portfolio_optimizer.py +834 -0
  109. mcli/ml/preprocessing/data_cleaners.py +451 -0
  110. mcli/ml/preprocessing/feature_extractors.py +491 -0
  111. mcli/ml/preprocessing/ml_pipeline.py +382 -0
  112. mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
  113. mcli/ml/preprocessing/test_preprocessing.py +294 -0
  114. mcli/ml/scripts/populate_sample_data.py +200 -0
  115. mcli/ml/tasks.py +400 -0
  116. mcli/ml/tests/test_integration.py +429 -0
  117. mcli/ml/tests/test_training_dashboard.py +387 -0
  118. mcli/public/oi/oi.py +15 -0
  119. mcli/public/public.py +4 -0
  120. mcli/self/self_cmd.py +1246 -0
  121. mcli/workflow/daemon/api_daemon.py +800 -0
  122. mcli/workflow/daemon/async_command_database.py +681 -0
  123. mcli/workflow/daemon/async_process_manager.py +591 -0
  124. mcli/workflow/daemon/client.py +530 -0
  125. mcli/workflow/daemon/commands.py +1196 -0
  126. mcli/workflow/daemon/daemon.py +905 -0
  127. mcli/workflow/daemon/daemon_api.py +59 -0
  128. mcli/workflow/daemon/enhanced_daemon.py +571 -0
  129. mcli/workflow/daemon/process_cli.py +244 -0
  130. mcli/workflow/daemon/process_manager.py +439 -0
  131. mcli/workflow/daemon/test_daemon.py +275 -0
  132. mcli/workflow/dashboard/dashboard_cmd.py +113 -0
  133. mcli/workflow/docker/docker.py +0 -0
  134. mcli/workflow/file/file.py +100 -0
  135. mcli/workflow/gcloud/config.toml +21 -0
  136. mcli/workflow/gcloud/gcloud.py +58 -0
  137. mcli/workflow/git_commit/ai_service.py +328 -0
  138. mcli/workflow/git_commit/commands.py +430 -0
  139. mcli/workflow/lsh_integration.py +355 -0
  140. mcli/workflow/model_service/client.py +594 -0
  141. mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
  142. mcli/workflow/model_service/lightweight_embedder.py +397 -0
  143. mcli/workflow/model_service/lightweight_model_server.py +714 -0
  144. mcli/workflow/model_service/lightweight_test.py +241 -0
  145. mcli/workflow/model_service/model_service.py +1955 -0
  146. mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
  147. mcli/workflow/model_service/pdf_processor.py +386 -0
  148. mcli/workflow/model_service/test_efficient_runner.py +234 -0
  149. mcli/workflow/model_service/test_example.py +315 -0
  150. mcli/workflow/model_service/test_integration.py +131 -0
  151. mcli/workflow/model_service/test_new_features.py +149 -0
  152. mcli/workflow/openai/openai.py +99 -0
  153. mcli/workflow/politician_trading/commands.py +1790 -0
  154. mcli/workflow/politician_trading/config.py +134 -0
  155. mcli/workflow/politician_trading/connectivity.py +490 -0
  156. mcli/workflow/politician_trading/data_sources.py +395 -0
  157. mcli/workflow/politician_trading/database.py +410 -0
  158. mcli/workflow/politician_trading/demo.py +248 -0
  159. mcli/workflow/politician_trading/models.py +165 -0
  160. mcli/workflow/politician_trading/monitoring.py +413 -0
  161. mcli/workflow/politician_trading/scrapers.py +966 -0
  162. mcli/workflow/politician_trading/scrapers_california.py +412 -0
  163. mcli/workflow/politician_trading/scrapers_eu.py +377 -0
  164. mcli/workflow/politician_trading/scrapers_uk.py +350 -0
  165. mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
  166. mcli/workflow/politician_trading/supabase_functions.py +354 -0
  167. mcli/workflow/politician_trading/workflow.py +852 -0
  168. mcli/workflow/registry/registry.py +180 -0
  169. mcli/workflow/repo/repo.py +223 -0
  170. mcli/workflow/scheduler/commands.py +493 -0
  171. mcli/workflow/scheduler/cron_parser.py +238 -0
  172. mcli/workflow/scheduler/job.py +182 -0
  173. mcli/workflow/scheduler/monitor.py +139 -0
  174. mcli/workflow/scheduler/persistence.py +324 -0
  175. mcli/workflow/scheduler/scheduler.py +679 -0
  176. mcli/workflow/sync/sync_cmd.py +437 -0
  177. mcli/workflow/sync/test_cmd.py +314 -0
  178. mcli/workflow/videos/videos.py +242 -0
  179. mcli/workflow/wakatime/wakatime.py +11 -0
  180. mcli/workflow/workflow.py +37 -0
  181. mcli_framework-7.0.0.dist-info/METADATA +479 -0
  182. mcli_framework-7.0.0.dist-info/RECORD +186 -0
  183. mcli_framework-7.0.0.dist-info/WHEEL +5 -0
  184. mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
  185. mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
  186. mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,437 @@
1
+ """Multi-cloud synchronization commands for mcli."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional
11
+
12
+ import click
13
+
14
+ from mcli.lib.logger.logger import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class MultiCloudSync:
20
+ """Handles synchronization across GitHub, OneDrive, iCloud, and Google Drive."""
21
+
22
+ def __init__(self, vault_path: str):
23
+ self.vault_path = Path(vault_path).resolve()
24
+ self.sync_config_path = self.vault_path / ".mcli_sync_config.json"
25
+ self.sync_log_path = self.vault_path / ".mcli_sync_log.json"
26
+
27
+ # Cloud storage paths - these will need to be configured by user
28
+ self.cloud_paths = {"onedrive": None, "icloud": None, "googledrive": None}
29
+
30
+ self.load_config()
31
+
32
+ def load_config(self) -> None:
33
+ """Load sync configuration from file."""
34
+ if self.sync_config_path.exists():
35
+ try:
36
+ with open(self.sync_config_path, "r") as f:
37
+ config = json.load(f)
38
+ self.cloud_paths.update(config.get("cloud_paths", {}))
39
+ logger.info(f"Loaded sync config from {self.sync_config_path}")
40
+ except Exception as e:
41
+ logger.warning(f"Failed to load sync config: {e}")
42
+
43
+ def save_config(self) -> None:
44
+ """Save sync configuration to file."""
45
+ config = {
46
+ "cloud_paths": self.cloud_paths,
47
+ "vault_path": str(self.vault_path),
48
+ "last_updated": datetime.now().isoformat(),
49
+ }
50
+ try:
51
+ with open(self.sync_config_path, "w") as f:
52
+ json.dump(config, f, indent=2)
53
+ logger.info(f"Saved sync config to {self.sync_config_path}")
54
+ except Exception as e:
55
+ logger.error(f"Failed to save sync config: {e}")
56
+
57
+ def log_sync_action(self, action: str, target: str, status: str, details: str = "") -> None:
58
+ """Log sync actions for debugging and auditing."""
59
+ log_entry = {
60
+ "timestamp": datetime.now().isoformat(),
61
+ "action": action,
62
+ "target": target,
63
+ "status": status,
64
+ "details": details,
65
+ }
66
+
67
+ # Load existing log
68
+ logs = []
69
+ if self.sync_log_path.exists():
70
+ try:
71
+ with open(self.sync_log_path, "r") as f:
72
+ logs = json.load(f)
73
+ except Exception as e:
74
+ logger.warning(f"Failed to load sync log: {e}")
75
+
76
+ # Append new entry and keep only last 100 entries
77
+ logs.append(log_entry)
78
+ logs = logs[-100:]
79
+
80
+ # Save log
81
+ try:
82
+ with open(self.sync_log_path, "w") as f:
83
+ json.dump(logs, f, indent=2)
84
+ except Exception as e:
85
+ logger.warning(f"Failed to save sync log: {e}")
86
+
87
+ def sync_to_github(self) -> bool:
88
+ """Sync vault to GitHub repository."""
89
+ try:
90
+ # Check if we're in a git repository
91
+ result = subprocess.run(
92
+ ["git", "status"], cwd=self.vault_path, capture_output=True, text=True
93
+ )
94
+ if result.returncode != 0:
95
+ self.log_sync_action("git_sync", "github", "error", "Not a git repository")
96
+ return False
97
+
98
+ # Add all changes
99
+ subprocess.run(["git", "add", "."], cwd=self.vault_path, check=True)
100
+
101
+ # Check if there are changes to commit
102
+ result = subprocess.run(
103
+ ["git", "diff", "--staged", "--quiet"], cwd=self.vault_path, capture_output=True
104
+ )
105
+ if result.returncode == 0:
106
+ self.log_sync_action("git_sync", "github", "success", "No changes to commit")
107
+ return True
108
+
109
+ # Commit changes
110
+ commit_msg = f"Auto-sync vault - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
111
+ subprocess.run(["git", "commit", "-m", commit_msg], cwd=self.vault_path, check=True)
112
+
113
+ # Push to remote
114
+ subprocess.run(["git", "push"], cwd=self.vault_path, check=True)
115
+
116
+ self.log_sync_action(
117
+ "git_sync", "github", "success", f"Committed and pushed: {commit_msg}"
118
+ )
119
+ return True
120
+
121
+ except subprocess.CalledProcessError as e:
122
+ self.log_sync_action("git_sync", "github", "error", str(e))
123
+ logger.error(f"Git sync failed: {e}")
124
+ return False
125
+
126
+ def sync_to_cloud_storage(self, cloud_name: str) -> bool:
127
+ """Sync vault to specified cloud storage."""
128
+ cloud_path = self.cloud_paths.get(cloud_name)
129
+ if not cloud_path:
130
+ self.log_sync_action("cloud_sync", cloud_name, "error", "Cloud path not configured")
131
+ return False
132
+
133
+ cloud_path = Path(cloud_path)
134
+ if not cloud_path.exists():
135
+ try:
136
+ cloud_path.mkdir(parents=True, exist_ok=True)
137
+ self.log_sync_action(
138
+ "cloud_sync", cloud_name, "info", f"Created directory: {cloud_path}"
139
+ )
140
+ except Exception as e:
141
+ self.log_sync_action(
142
+ "cloud_sync", cloud_name, "error", f"Failed to create directory: {e}"
143
+ )
144
+ return False
145
+
146
+ try:
147
+ # Use rsync for efficient sync (macOS has rsync built-in)
148
+ result = subprocess.run(
149
+ [
150
+ "rsync",
151
+ "-av",
152
+ "--delete",
153
+ "--exclude=.git",
154
+ "--exclude=.obsidian/workspace*",
155
+ "--exclude=.mcli_sync_*",
156
+ "--exclude=__pycache__",
157
+ "--exclude=*.pyc",
158
+ f"{self.vault_path}/",
159
+ f"{cloud_path}/",
160
+ ],
161
+ capture_output=True,
162
+ text=True,
163
+ )
164
+
165
+ if result.returncode == 0:
166
+ self.log_sync_action("cloud_sync", cloud_name, "success", f"Synced to {cloud_path}")
167
+ return True
168
+ else:
169
+ self.log_sync_action("cloud_sync", cloud_name, "error", result.stderr)
170
+ return False
171
+
172
+ except Exception as e:
173
+ self.log_sync_action("cloud_sync", cloud_name, "error", str(e))
174
+ logger.error(f"Cloud sync to {cloud_name} failed: {e}")
175
+ return False
176
+
177
+ def sync_from_cloud_storage(self, cloud_name: str) -> bool:
178
+ """Sync from cloud storage to vault."""
179
+ cloud_path = self.cloud_paths.get(cloud_name)
180
+ if not cloud_path or not Path(cloud_path).exists():
181
+ self.log_sync_action("cloud_pull", cloud_name, "error", "Cloud path not found")
182
+ return False
183
+
184
+ try:
185
+ # Use rsync to pull changes from cloud
186
+ result = subprocess.run(
187
+ [
188
+ "rsync",
189
+ "-av",
190
+ "--exclude=.git",
191
+ "--exclude=.obsidian/workspace*",
192
+ "--exclude=.mcli_sync_*",
193
+ "--exclude=__pycache__",
194
+ "--exclude=*.pyc",
195
+ f"{cloud_path}/",
196
+ f"{self.vault_path}/",
197
+ ],
198
+ capture_output=True,
199
+ text=True,
200
+ )
201
+
202
+ if result.returncode == 0:
203
+ self.log_sync_action(
204
+ "cloud_pull", cloud_name, "success", f"Pulled from {cloud_path}"
205
+ )
206
+ return True
207
+ else:
208
+ self.log_sync_action("cloud_pull", cloud_name, "error", result.stderr)
209
+ return False
210
+
211
+ except Exception as e:
212
+ self.log_sync_action("cloud_pull", cloud_name, "error", str(e))
213
+ logger.error(f"Cloud pull from {cloud_name} failed: {e}")
214
+ return False
215
+
216
+ def get_sync_status(self) -> Dict:
217
+ """Get current sync status for all configured targets."""
218
+ status = {
219
+ "vault_path": str(self.vault_path),
220
+ "last_check": datetime.now().isoformat(),
221
+ "targets": {},
222
+ }
223
+
224
+ # Check git status
225
+ try:
226
+ result = subprocess.run(
227
+ ["git", "status", "--porcelain"],
228
+ cwd=self.vault_path,
229
+ capture_output=True,
230
+ text=True,
231
+ )
232
+ if result.returncode == 0:
233
+ has_changes = bool(result.stdout.strip())
234
+ status["targets"]["github"] = {
235
+ "configured": True,
236
+ "has_uncommitted_changes": has_changes,
237
+ "status": "dirty" if has_changes else "clean",
238
+ }
239
+ else:
240
+ status["targets"]["github"] = {"configured": False, "error": "Not a git repository"}
241
+ except Exception as e:
242
+ status["targets"]["github"] = {"configured": False, "error": str(e)}
243
+
244
+ # Check cloud storage paths
245
+ for cloud_name, cloud_path in self.cloud_paths.items():
246
+ if cloud_path:
247
+ path_obj = Path(cloud_path)
248
+ status["targets"][cloud_name] = {
249
+ "configured": True,
250
+ "path": cloud_path,
251
+ "exists": path_obj.exists(),
252
+ "accessible": path_obj.exists() and os.access(path_obj, os.R_OK | os.W_OK),
253
+ }
254
+ else:
255
+ status["targets"][cloud_name] = {"configured": False, "path": None}
256
+
257
+ return status
258
+
259
+
260
+ @click.group(name="sync")
261
+ def sync():
262
+ """Multi-cloud synchronization commands for vault management."""
263
+ pass
264
+
265
+
266
+ # Import and register test commands
267
+ try:
268
+ from .test_cmd import test as sync_test
269
+
270
+ sync.add_command(sync_test)
271
+ except ImportError:
272
+ pass
273
+
274
+
275
+ @sync.command()
276
+ @click.option("--vault-path", default=".", help="Path to vault directory")
277
+ def status(vault_path):
278
+ """Show sync status for all configured targets."""
279
+ syncer = MultiCloudSync(vault_path)
280
+ status_info = syncer.get_sync_status()
281
+
282
+ click.echo(f"šŸ“ Vault: {status_info['vault_path']}")
283
+ click.echo(f"šŸ•’ Last check: {status_info['last_check']}")
284
+ click.echo()
285
+
286
+ for target, info in status_info["targets"].items():
287
+ if info["configured"]:
288
+ if target == "github":
289
+ icon = "šŸ“š" if info["status"] == "clean" else "āš ļø"
290
+ click.echo(f"{icon} {target.title()}: {info['status']}")
291
+ if info.get("has_uncommitted_changes"):
292
+ click.echo(f" └─ Uncommitted changes present")
293
+ else:
294
+ icon = "ā˜ļø" if info["accessible"] else "āŒ"
295
+ click.echo(
296
+ f"{icon} {target.title()}: {'accessible' if info['accessible'] else 'not accessible'}"
297
+ )
298
+ click.echo(f" └─ Path: {info['path']}")
299
+ else:
300
+ click.echo(f"⚪ {target.title()}: not configured")
301
+ if "error" in info:
302
+ click.echo(f" └─ Error: {info['error']}")
303
+
304
+
305
+ @sync.command()
306
+ @click.option("--vault-path", default=".", help="Path to vault directory")
307
+ @click.option("--onedrive", help="Path to OneDrive sync folder")
308
+ @click.option("--icloud", help="Path to iCloud Drive sync folder")
309
+ @click.option("--googledrive", help="Path to Google Drive sync folder")
310
+ def configure(vault_path, onedrive, icloud, googledrive):
311
+ """Configure cloud storage paths for synchronization."""
312
+ syncer = MultiCloudSync(vault_path)
313
+
314
+ if onedrive:
315
+ syncer.cloud_paths["onedrive"] = onedrive
316
+ click.echo(f"āœ… OneDrive path set to: {onedrive}")
317
+
318
+ if icloud:
319
+ syncer.cloud_paths["icloud"] = icloud
320
+ click.echo(f"āœ… iCloud path set to: {icloud}")
321
+
322
+ if googledrive:
323
+ syncer.cloud_paths["googledrive"] = googledrive
324
+ click.echo(f"āœ… Google Drive path set to: {googledrive}")
325
+
326
+ syncer.save_config()
327
+ click.echo("šŸ”§ Configuration saved!")
328
+
329
+
330
+ @sync.command()
331
+ @click.option("--vault-path", default=".", help="Path to vault directory")
332
+ @click.option(
333
+ "--target",
334
+ type=click.Choice(["all", "github", "onedrive", "icloud", "googledrive"]),
335
+ default="all",
336
+ help="Sync target",
337
+ )
338
+ def push(vault_path, target):
339
+ """Push vault changes to specified target(s)."""
340
+ syncer = MultiCloudSync(vault_path)
341
+
342
+ targets = [target] if target != "all" else ["github", "onedrive", "icloud", "googledrive"]
343
+
344
+ results = {}
345
+
346
+ for t in targets:
347
+ if t == "github":
348
+ click.echo(f"šŸ”„ Syncing to GitHub...")
349
+ results[t] = syncer.sync_to_github()
350
+ else:
351
+ if syncer.cloud_paths.get(t):
352
+ click.echo(f"šŸ”„ Syncing to {t.title()}...")
353
+ results[t] = syncer.sync_to_cloud_storage(t)
354
+ else:
355
+ click.echo(f"āš ļø {t.title()} not configured, skipping...")
356
+ results[t] = False
357
+
358
+ # Show results
359
+ click.echo("\nšŸ“Š Sync Results:")
360
+ for target_name, success in results.items():
361
+ icon = "āœ…" if success else "āŒ"
362
+ click.echo(f"{icon} {target_name.title()}: {'Success' if success else 'Failed'}")
363
+
364
+
365
+ @sync.command()
366
+ @click.option("--vault-path", default=".", help="Path to vault directory")
367
+ @click.option(
368
+ "--target",
369
+ type=click.Choice(["onedrive", "icloud", "googledrive"]),
370
+ required=True,
371
+ help="Cloud storage to pull from",
372
+ )
373
+ def pull(vault_path, target):
374
+ """Pull changes from cloud storage to vault."""
375
+ syncer = MultiCloudSync(vault_path)
376
+
377
+ if not syncer.cloud_paths.get(target):
378
+ click.echo(f"āŒ {target.title()} not configured!")
379
+ return
380
+
381
+ click.echo(f"šŸ”„ Pulling from {target.title()}...")
382
+ success = syncer.sync_from_cloud_storage(target)
383
+
384
+ if success:
385
+ click.echo(f"āœ… Successfully pulled from {target.title()}")
386
+ else:
387
+ click.echo(f"āŒ Failed to pull from {target.title()}")
388
+
389
+
390
+ @sync.command()
391
+ @click.option("--vault-path", default=".", help="Path to vault directory")
392
+ @click.option("--lines", default=20, help="Number of log lines to show")
393
+ def logs(vault_path, lines):
394
+ """Show sync operation logs."""
395
+ syncer = MultiCloudSync(vault_path)
396
+
397
+ if not syncer.sync_log_path.exists():
398
+ click.echo("šŸ“ No sync logs found.")
399
+ return
400
+
401
+ try:
402
+ with open(syncer.sync_log_path, "r") as f:
403
+ logs_data = json.load(f)
404
+
405
+ # Show last N entries
406
+ recent_logs = logs_data[-lines:]
407
+
408
+ click.echo(f"šŸ“ Last {len(recent_logs)} sync operations:")
409
+ click.echo()
410
+
411
+ for log_entry in recent_logs:
412
+ timestamp = log_entry.get("timestamp", "Unknown")
413
+ action = log_entry.get("action", "Unknown")
414
+ target = log_entry.get("target", "Unknown")
415
+ status = log_entry.get("status", "Unknown")
416
+ details = log_entry.get("details", "")
417
+
418
+ # Format timestamp
419
+ try:
420
+ dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
421
+ time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
422
+ except:
423
+ time_str = timestamp
424
+
425
+ # Status icon
426
+ icon = {"success": "āœ…", "error": "āŒ", "info": "ā„¹ļø"}.get(status, "šŸ“")
427
+
428
+ click.echo(f"{icon} {time_str} | {action} → {target} | {status}")
429
+ if details:
430
+ click.echo(f" └─ {details}")
431
+
432
+ except Exception as e:
433
+ click.echo(f"āŒ Failed to read logs: {e}")
434
+
435
+
436
+ if __name__ == "__main__":
437
+ sync()