mcli-framework 7.8.5__py3-none-any.whl → 7.9.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.

mcli/app/main.py CHANGED
@@ -344,6 +344,15 @@ 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}")
355
+
347
356
  # Add workflow with completion-aware lazy loading
348
357
  try:
349
358
  from mcli.app.completion_helpers import create_completion_aware_lazy_group
mcli/lib/lib.py CHANGED
@@ -3,6 +3,8 @@ import sys
3
3
 
4
4
  import click
5
5
 
6
+ from mcli.lib.secrets.commands import secrets_group
7
+
6
8
 
7
9
  def import_public_module(module_name: str):
8
10
  prefix = "mcli.public."
@@ -20,10 +22,15 @@ def import_public_module(module_name: str):
20
22
  return module
21
23
 
22
24
 
23
- @click.group(name="lib")
25
+ @click.group(name="lib", help="Library utilities and secrets management")
24
26
  def lib():
27
+ """Library utilities and management commands."""
25
28
  pass
26
29
 
27
30
 
31
+ # Add secrets as a subcommand
32
+ lib.add_command(secrets_group)
33
+
34
+
28
35
  if __name__ == "__main__":
29
36
  lib()
@@ -0,0 +1,10 @@
1
+ """
2
+ Secrets management module for MCLI.
3
+
4
+ Provides secure storage and retrieval of secrets with git-based synchronization.
5
+ """
6
+
7
+ from .manager import SecretsManager
8
+ from .store import SecretsStore
9
+
10
+ __all__ = ["SecretsManager", "SecretsStore"]
@@ -0,0 +1,185 @@
1
+ """
2
+ CLI commands for secrets management.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import click
9
+
10
+ from mcli.lib.ui.styling import error, info, success, warning
11
+
12
+ from .manager import SecretsManager
13
+ from .repl import run_repl
14
+ from .store import SecretsStore
15
+
16
+
17
+ @click.group(name="secrets", help="Secure secrets management with encryption and git sync")
18
+ def secrets_group():
19
+ """Secrets management commands."""
20
+ pass
21
+
22
+
23
+ @secrets_group.command(name="repl", help="Launch interactive secrets shell")
24
+ def secrets_repl():
25
+ """Launch the interactive secrets REPL."""
26
+ run_repl()
27
+
28
+
29
+ @secrets_group.command(name="set", help="Set a secret value")
30
+ @click.argument("key")
31
+ @click.argument("value")
32
+ @click.option("-n", "--namespace", default="default", help="Namespace for the secret")
33
+ def secrets_set(key: str, value: str, namespace: str):
34
+ """Set a secret value."""
35
+ manager = SecretsManager()
36
+ try:
37
+ manager.set(key, value, namespace)
38
+ success(f"Secret '{key}' set in namespace '{namespace}'")
39
+ except Exception as e:
40
+ error(f"Failed to set secret: {e}")
41
+
42
+
43
+ @secrets_group.command(name="get", help="Get a secret value")
44
+ @click.argument("key")
45
+ @click.option("-n", "--namespace", default="default", help="Namespace for the secret")
46
+ @click.option("-s", "--show", is_flag=True, help="Show the full value (not masked)")
47
+ def secrets_get(key: str, namespace: str, show: bool):
48
+ """Get a secret value."""
49
+ manager = SecretsManager()
50
+ value = manager.get(key, namespace)
51
+
52
+ if value is not None:
53
+ if show:
54
+ click.echo(value)
55
+ else:
56
+ # Mask the value
57
+ masked = (
58
+ value[:3] + "*" * (len(value) - 6) + value[-3:]
59
+ if len(value) > 6
60
+ else "*" * len(value)
61
+ )
62
+ info(f"{key} = {masked}")
63
+ info("Use --show to display the full value")
64
+ else:
65
+ warning(f"Secret '{key}' not found in namespace '{namespace}'")
66
+
67
+
68
+ @secrets_group.command(name="list", help="List all secrets")
69
+ @click.option("-n", "--namespace", help="Filter by namespace")
70
+ def secrets_list(namespace: Optional[str]):
71
+ """List all secrets."""
72
+ manager = SecretsManager()
73
+ secrets = manager.list(namespace)
74
+
75
+ if secrets:
76
+ info("Secrets:")
77
+ for secret in secrets:
78
+ click.echo(f" • {secret}")
79
+ else:
80
+ info("No secrets found")
81
+
82
+
83
+ @secrets_group.command(name="delete", help="Delete a secret")
84
+ @click.argument("key")
85
+ @click.option("-n", "--namespace", default="default", help="Namespace for the secret")
86
+ @click.confirmation_option(prompt="Are you sure you want to delete this secret?")
87
+ def secrets_delete(key: str, namespace: str):
88
+ """Delete a secret."""
89
+ manager = SecretsManager()
90
+ if manager.delete(key, namespace):
91
+ success(f"Secret '{key}' deleted from namespace '{namespace}'")
92
+ else:
93
+ warning(f"Secret '{key}' not found in namespace '{namespace}'")
94
+
95
+
96
+ @secrets_group.command(name="export", help="Export secrets as environment variables")
97
+ @click.option("-n", "--namespace", help="Namespace to export")
98
+ @click.option("-o", "--output", type=click.Path(), help="Output file (defaults to stdout)")
99
+ def secrets_export(namespace: Optional[str], output: Optional[str]):
100
+ """Export secrets as environment variables."""
101
+ manager = SecretsManager()
102
+ env_vars = manager.export_env(namespace)
103
+
104
+ if env_vars:
105
+ if output:
106
+ with open(output, "w") as f:
107
+ for key, value in env_vars.items():
108
+ f.write(f"export {key}={value}\n")
109
+ success(f"Exported {len(env_vars)} secrets to {output}")
110
+ else:
111
+ for key, value in env_vars.items():
112
+ click.echo(f"export {key}={value}")
113
+ else:
114
+ info("No secrets to export")
115
+
116
+
117
+ @secrets_group.command(name="import", help="Import secrets from environment file")
118
+ @click.argument("env_file", type=click.Path(exists=True))
119
+ @click.option("-n", "--namespace", default="default", help="Namespace to import into")
120
+ def secrets_import(env_file: str, namespace: str):
121
+ """Import secrets from environment file."""
122
+ manager = SecretsManager()
123
+ count = manager.import_env(Path(env_file), namespace)
124
+ success(f"Imported {count} secrets into namespace '{namespace}'")
125
+
126
+
127
+ @secrets_group.group(name="store", help="Git-based secrets synchronization")
128
+ def store_group():
129
+ """Store management commands."""
130
+ pass
131
+
132
+
133
+ @store_group.command(name="init", help="Initialize secrets store")
134
+ @click.option("-r", "--remote", help="Git remote URL")
135
+ def store_init(remote: Optional[str]):
136
+ """Initialize the secrets store."""
137
+ store = SecretsStore()
138
+ store.init(remote)
139
+
140
+
141
+ @store_group.command(name="push", help="Push secrets to store")
142
+ @click.option("-m", "--message", help="Commit message")
143
+ def store_push(message: Optional[str]):
144
+ """Push secrets to store."""
145
+ manager = SecretsManager()
146
+ store = SecretsStore()
147
+ store.push(manager.secrets_dir, message)
148
+
149
+
150
+ @store_group.command(name="pull", help="Pull secrets from store")
151
+ def store_pull():
152
+ """Pull secrets from store."""
153
+ manager = SecretsManager()
154
+ store = SecretsStore()
155
+ store.pull(manager.secrets_dir)
156
+
157
+
158
+ @store_group.command(name="sync", help="Sync secrets with store")
159
+ @click.option("-m", "--message", help="Commit message")
160
+ def store_sync(message: Optional[str]):
161
+ """Sync secrets with store."""
162
+ manager = SecretsManager()
163
+ store = SecretsStore()
164
+ store.sync(manager.secrets_dir, message)
165
+
166
+
167
+ @store_group.command(name="status", help="Show store status")
168
+ def store_status():
169
+ """Show store status."""
170
+ store = SecretsStore()
171
+ status = store.status()
172
+
173
+ info("Secrets Store Status:")
174
+ click.echo(f" Initialized: {status['initialized']}")
175
+ click.echo(f" Path: {status['store_path']}")
176
+
177
+ if status["initialized"]:
178
+ click.echo(f" Branch: {status['branch']}")
179
+ click.echo(f" Commit: {status['commit']}")
180
+ click.echo(f" Clean: {status['clean']}")
181
+
182
+ if status["has_remote"]:
183
+ click.echo(f" Remote: {status['remote_url']}")
184
+ else:
185
+ click.echo(" Remote: Not configured")
@@ -0,0 +1,213 @@
1
+ """
2
+ Secrets manager for handling secure storage and retrieval of secrets.
3
+ """
4
+
5
+ import base64
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional
10
+
11
+ import click
12
+ from cryptography.fernet import Fernet
13
+ from cryptography.hazmat.primitives import hashes
14
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
15
+
16
+ from mcli.lib.logger.logger import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class SecretsManager:
22
+ """Manages secrets storage with encryption."""
23
+
24
+ def __init__(self, secrets_dir: Optional[Path] = None):
25
+ """Initialize the secrets manager.
26
+
27
+ Args:
28
+ secrets_dir: Directory to store secrets. Defaults to ~/.mcli/secrets/
29
+ """
30
+ self.secrets_dir = secrets_dir or Path.home() / ".mcli" / "secrets"
31
+ self.secrets_dir.mkdir(parents=True, exist_ok=True)
32
+ self._cipher_suite = self._get_cipher_suite()
33
+
34
+ def _get_cipher_suite(self) -> Fernet:
35
+ """Get or create encryption key."""
36
+ key_file = self.secrets_dir / ".key"
37
+
38
+ if key_file.exists():
39
+ with open(key_file, "rb") as f:
40
+ key = f.read()
41
+ else:
42
+ # Generate a new key from a password
43
+ password = click.prompt("Enter a password for secrets encryption", hide_input=True)
44
+ password_bytes = password.encode()
45
+
46
+ # Use PBKDF2 to derive a key from the password
47
+ kdf = PBKDF2HMAC(
48
+ algorithm=hashes.SHA256(),
49
+ length=32,
50
+ salt=b"mcli-secrets-salt", # In production, use a random salt
51
+ iterations=100000,
52
+ )
53
+ key = base64.urlsafe_b64encode(kdf.derive(password_bytes))
54
+
55
+ # Save the key (in production, this should be stored more securely)
56
+ with open(key_file, "wb") as f:
57
+ f.write(key)
58
+
59
+ # Set restrictive permissions
60
+ os.chmod(key_file, 0o600)
61
+
62
+ return Fernet(key)
63
+
64
+ def set(self, key: str, value: str, namespace: Optional[str] = None) -> None:
65
+ """Set a secret value.
66
+
67
+ Args:
68
+ key: Secret key
69
+ value: Secret value
70
+ namespace: Optional namespace for grouping secrets
71
+ """
72
+ namespace = namespace or "default"
73
+ namespace_dir = self.secrets_dir / namespace
74
+ namespace_dir.mkdir(exist_ok=True)
75
+
76
+ # Encrypt the value
77
+ encrypted_value = self._cipher_suite.encrypt(value.encode())
78
+
79
+ # Store the encrypted value
80
+ secret_file = namespace_dir / f"{key}.secret"
81
+ with open(secret_file, "wb") as f:
82
+ f.write(encrypted_value)
83
+
84
+ # Set restrictive permissions
85
+ os.chmod(secret_file, 0o600)
86
+
87
+ logger.debug(f"Secret '{key}' stored in namespace '{namespace}'")
88
+
89
+ def get(self, key: str, namespace: Optional[str] = None) -> Optional[str]:
90
+ """Get a secret value.
91
+
92
+ Args:
93
+ key: Secret key
94
+ namespace: Optional namespace
95
+
96
+ Returns:
97
+ Decrypted secret value or None if not found
98
+ """
99
+ namespace = namespace or "default"
100
+ secret_file = self.secrets_dir / namespace / f"{key}.secret"
101
+
102
+ if not secret_file.exists():
103
+ return None
104
+
105
+ with open(secret_file, "rb") as f:
106
+ encrypted_value = f.read()
107
+
108
+ try:
109
+ decrypted_value = self._cipher_suite.decrypt(encrypted_value)
110
+ return decrypted_value.decode()
111
+ except Exception as e:
112
+ logger.error(f"Failed to decrypt secret '{key}': {e}")
113
+ return None
114
+
115
+ def list(self, namespace: Optional[str] = None) -> List[str]:
116
+ """List all secret keys.
117
+
118
+ Args:
119
+ namespace: Optional namespace filter
120
+
121
+ Returns:
122
+ List of secret keys
123
+ """
124
+ if namespace:
125
+ namespace_dirs = [self.secrets_dir / namespace]
126
+ else:
127
+ namespace_dirs = [
128
+ d for d in self.secrets_dir.iterdir() if d.is_dir() and not d.name.startswith(".")
129
+ ]
130
+
131
+ secrets = []
132
+ for namespace_dir in namespace_dirs:
133
+ if namespace_dir.exists():
134
+ for secret_file in namespace_dir.glob("*.secret"):
135
+ key = secret_file.stem
136
+ ns = namespace_dir.name
137
+ secrets.append(f"{ns}/{key}" if not namespace else key)
138
+
139
+ return sorted(secrets)
140
+
141
+ def delete(self, key: str, namespace: Optional[str] = None) -> bool:
142
+ """Delete a secret.
143
+
144
+ Args:
145
+ key: Secret key
146
+ namespace: Optional namespace
147
+
148
+ Returns:
149
+ True if deleted, False if not found
150
+ """
151
+ namespace = namespace or "default"
152
+ secret_file = self.secrets_dir / namespace / f"{key}.secret"
153
+
154
+ if secret_file.exists():
155
+ secret_file.unlink()
156
+ logger.debug(f"Secret '{key}' deleted from namespace '{namespace}'")
157
+ return True
158
+
159
+ return False
160
+
161
+ def export_env(self, namespace: Optional[str] = None) -> Dict[str, str]:
162
+ """Export secrets as environment variables.
163
+
164
+ Args:
165
+ namespace: Optional namespace filter
166
+
167
+ Returns:
168
+ Dictionary of key-value pairs
169
+ """
170
+ env_vars = {}
171
+
172
+ for secret_key in self.list(namespace):
173
+ if "/" in secret_key:
174
+ ns, key = secret_key.split("/", 1)
175
+ value = self.get(key, ns)
176
+ else:
177
+ value = self.get(secret_key, namespace)
178
+
179
+ if value:
180
+ # Convert to uppercase for environment variable convention
181
+ if "/" in secret_key:
182
+ env_key = key.upper().replace("-", "_")
183
+ else:
184
+ env_key = secret_key.upper().replace("-", "_")
185
+ env_vars[env_key] = value
186
+
187
+ return env_vars
188
+
189
+ def import_env(self, env_file: Path, namespace: Optional[str] = None) -> int:
190
+ """Import secrets from an environment file.
191
+
192
+ Args:
193
+ env_file: Path to .env file
194
+ namespace: Optional namespace
195
+
196
+ Returns:
197
+ Number of secrets imported
198
+ """
199
+ namespace = namespace or "default"
200
+ count = 0
201
+
202
+ with open(env_file) as f:
203
+ for line in f:
204
+ line = line.strip()
205
+ if line and not line.startswith("#") and "=" in line:
206
+ key, value = line.split("=", 1)
207
+ key = key.strip()
208
+ value = value.strip().strip('"').strip("'")
209
+
210
+ self.set(key.lower().replace("_", "-"), value, namespace)
211
+ count += 1
212
+
213
+ return count
@@ -0,0 +1,297 @@
1
+ """
2
+ REPL (Read-Eval-Print Loop) for LSH secrets management.
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+ import click
10
+ from prompt_toolkit import prompt
11
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
12
+ from prompt_toolkit.completion import WordCompleter
13
+ from prompt_toolkit.history import FileHistory
14
+
15
+ from mcli.lib.logger.logger import get_logger
16
+
17
+ logger = get_logger(__name__)
18
+ from mcli.lib.ui.styling import console, error, info, success, warning
19
+
20
+ from .manager import SecretsManager
21
+ from .store import SecretsStore
22
+
23
+
24
+ class SecretsREPL:
25
+ """Interactive REPL for secrets management."""
26
+
27
+ def __init__(self):
28
+ """Initialize the REPL."""
29
+ self.manager = SecretsManager()
30
+ self.store = SecretsStore()
31
+ self.running = False
32
+ self.namespace = "default"
33
+ self.history_file = Path.home() / ".mcli" / "secrets_repl_history"
34
+
35
+ # Commands
36
+ self.commands = {
37
+ "set": self.cmd_set,
38
+ "get": self.cmd_get,
39
+ "list": self.cmd_list,
40
+ "delete": self.cmd_delete,
41
+ "namespace": self.cmd_namespace,
42
+ "export": self.cmd_export,
43
+ "import": self.cmd_import,
44
+ "push": self.cmd_push,
45
+ "pull": self.cmd_pull,
46
+ "sync": self.cmd_sync,
47
+ "status": self.cmd_status,
48
+ "help": self.cmd_help,
49
+ "exit": self.cmd_exit,
50
+ "quit": self.cmd_exit,
51
+ }
52
+
53
+ # Command completer
54
+ self.completer = WordCompleter(list(self.commands.keys()) + ["ns"], ignore_case=True)
55
+
56
+ def run(self):
57
+ """Run the REPL."""
58
+ self.running = True
59
+
60
+ # Print welcome message
61
+ console.print("[bold cyan]MCLI Secrets Management Shell[/bold cyan]")
62
+ console.print("Type 'help' for available commands or 'exit' to quit.\n")
63
+
64
+ # Create history file directory if needed
65
+ self.history_file.parent.mkdir(parents=True, exist_ok=True)
66
+
67
+ while self.running:
68
+ try:
69
+ # Build prompt
70
+ prompt_text = f"[{self.namespace}]> "
71
+
72
+ # Get user input
73
+ user_input = prompt(
74
+ prompt_text,
75
+ completer=self.completer,
76
+ history=FileHistory(str(self.history_file)),
77
+ auto_suggest=AutoSuggestFromHistory(),
78
+ ).strip()
79
+
80
+ if not user_input:
81
+ continue
82
+
83
+ # Parse command and arguments
84
+ parts = user_input.split()
85
+ command = parts[0].lower()
86
+ args = parts[1:] if len(parts) > 1 else []
87
+
88
+ # Handle command aliases
89
+ if command == "ns":
90
+ command = "namespace"
91
+
92
+ # Execute command
93
+ if command in self.commands:
94
+ self.commands[command](args)
95
+ else:
96
+ error(f"Unknown command: {command}")
97
+ console.print("Type 'help' for available commands.")
98
+
99
+ except KeyboardInterrupt:
100
+ console.print("\nUse 'exit' or 'quit' to leave the shell.")
101
+ except EOFError:
102
+ self.cmd_exit([])
103
+ except Exception as e:
104
+ error(f"Error: {e}")
105
+ logger.exception("REPL error")
106
+
107
+ def cmd_set(self, args: List[str]):
108
+ """Set a secret value."""
109
+ if len(args) < 2:
110
+ error("Usage: set <key> <value>")
111
+ return
112
+
113
+ key = args[0]
114
+ value = " ".join(args[1:])
115
+
116
+ try:
117
+ self.manager.set(key, value, self.namespace)
118
+ success(f"Secret '{key}' set in namespace '{self.namespace}'")
119
+ except Exception as e:
120
+ error(f"Failed to set secret: {e}")
121
+
122
+ def cmd_get(self, args: List[str]):
123
+ """Get a secret value."""
124
+ if len(args) != 1:
125
+ error("Usage: get <key>")
126
+ return
127
+
128
+ key = args[0]
129
+ value = self.manager.get(key, self.namespace)
130
+
131
+ if value is not None:
132
+ # Mask the value for security
133
+ masked_value = (
134
+ value[:3] + "*" * (len(value) - 6) + value[-3:]
135
+ if len(value) > 6
136
+ else "*" * len(value)
137
+ )
138
+ info(f"{key} = {masked_value}")
139
+
140
+ if click.confirm("Show full value?", default=False):
141
+ console.print(f"[yellow]{value}[/yellow]")
142
+ else:
143
+ warning(f"Secret '{key}' not found in namespace '{self.namespace}'")
144
+
145
+ def cmd_list(self, args: List[str]):
146
+ """List all secrets."""
147
+ secrets = self.manager.list(self.namespace if args != ["all"] else None)
148
+
149
+ if secrets:
150
+ console.print("[bold]Secrets:[/bold]")
151
+ for secret in secrets:
152
+ console.print(f" • {secret}")
153
+ else:
154
+ info("No secrets found")
155
+
156
+ def cmd_delete(self, args: List[str]):
157
+ """Delete a secret."""
158
+ if len(args) != 1:
159
+ error("Usage: delete <key>")
160
+ return
161
+
162
+ key = args[0]
163
+
164
+ if click.confirm(f"Delete secret '{key}' from namespace '{self.namespace}'?"):
165
+ if self.manager.delete(key, self.namespace):
166
+ success(f"Secret '{key}' deleted")
167
+ else:
168
+ warning(f"Secret '{key}' not found")
169
+
170
+ def cmd_namespace(self, args: List[str]):
171
+ """Switch namespace."""
172
+ if len(args) == 0:
173
+ # List namespaces
174
+ namespaces = set()
175
+ for d in self.manager.secrets_dir.iterdir():
176
+ if d.is_dir() and not d.name.startswith("."):
177
+ namespaces.add(d.name)
178
+
179
+ console.print(f"[bold]Current namespace:[/bold] {self.namespace}")
180
+ if namespaces:
181
+ console.print("[bold]Available namespaces:[/bold]")
182
+ for ns in sorted(namespaces):
183
+ marker = "→" if ns == self.namespace else " "
184
+ console.print(f" {marker} {ns}")
185
+ elif len(args) == 1:
186
+ self.namespace = args[0]
187
+ success(f"Switched to namespace '{self.namespace}'")
188
+ else:
189
+ error("Usage: namespace [<name>]")
190
+
191
+ def cmd_export(self, args: List[str]):
192
+ """Export secrets as environment variables."""
193
+ env_vars = self.manager.export_env(self.namespace)
194
+
195
+ if env_vars:
196
+ if args and args[0] == "file":
197
+ # Export to file
198
+ filename = args[1] if len(args) > 1 else f"{self.namespace}.env"
199
+ with open(filename, "w") as f:
200
+ for key, value in env_vars.items():
201
+ f.write(f"{key}={value}\n")
202
+ success(f"Exported {len(env_vars)} secrets to {filename}")
203
+ else:
204
+ # Display export commands
205
+ console.print("[bold]Export commands:[/bold]")
206
+ for key, value in env_vars.items():
207
+ masked_value = value[:3] + "***" + value[-3:] if len(value) > 6 else "***"
208
+ console.print(f"export {key}={masked_value}")
209
+ else:
210
+ info("No secrets to export")
211
+
212
+ def cmd_import(self, args: List[str]):
213
+ """Import secrets from environment file."""
214
+ if len(args) != 1:
215
+ error("Usage: import <env-file>")
216
+ return
217
+
218
+ env_file = Path(args[0])
219
+ if not env_file.exists():
220
+ error(f"File not found: {env_file}")
221
+ return
222
+
223
+ count = self.manager.import_env(env_file, self.namespace)
224
+ success(f"Imported {count} secrets from {env_file}")
225
+
226
+ def cmd_push(self, args: List[str]):
227
+ """Push secrets to git store."""
228
+ message = " ".join(args) if args else None
229
+ self.store.push(self.manager.secrets_dir, message)
230
+
231
+ def cmd_pull(self, args: List[str]):
232
+ """Pull secrets from git store."""
233
+ self.store.pull(self.manager.secrets_dir)
234
+
235
+ def cmd_sync(self, args: List[str]):
236
+ """Sync secrets with git store."""
237
+ message = " ".join(args) if args else None
238
+ self.store.sync(self.manager.secrets_dir, message)
239
+
240
+ def cmd_status(self, args: List[str]):
241
+ """Show store status."""
242
+ status = self.store.status()
243
+
244
+ console.print("[bold]Secrets Store Status:[/bold]")
245
+ console.print(f" Initialized: {status['initialized']}")
246
+ console.print(f" Path: {status['store_path']}")
247
+
248
+ if status["initialized"]:
249
+ console.print(f" Branch: {status['branch']}")
250
+ console.print(f" Commit: {status['commit']}")
251
+ console.print(f" Clean: {status['clean']}")
252
+
253
+ if status["has_remote"]:
254
+ console.print(f" Remote: {status['remote_url']}")
255
+ else:
256
+ console.print(" Remote: [dim]Not configured[/dim]")
257
+
258
+ def cmd_help(self, args: List[str]):
259
+ """Show help information."""
260
+ console.print("[bold]Available Commands:[/bold]\n")
261
+
262
+ help_text = {
263
+ "set": "Set a secret value",
264
+ "get": "Get a secret value",
265
+ "list": "List all secrets (use 'list all' for all namespaces)",
266
+ "delete": "Delete a secret",
267
+ "namespace": "Switch namespace or list namespaces (alias: ns)",
268
+ "export": "Export secrets as environment variables",
269
+ "import": "Import secrets from .env file",
270
+ "push": "Push secrets to git store",
271
+ "pull": "Pull secrets from git store",
272
+ "sync": "Sync secrets with git store",
273
+ "status": "Show store status",
274
+ "help": "Show this help",
275
+ "exit": "Exit the shell (alias: quit)",
276
+ }
277
+
278
+ for cmd, desc in help_text.items():
279
+ console.print(f" [cyan]{cmd:12}[/cyan] {desc}")
280
+
281
+ console.print("\n[bold]Examples:[/bold]")
282
+ console.print(" set api-key sk-1234567890")
283
+ console.print(" get api-key")
284
+ console.print(" namespace production")
285
+ console.print(" export file production.env")
286
+ console.print(" import .env.local")
287
+
288
+ def cmd_exit(self, args: List[str]):
289
+ """Exit the REPL."""
290
+ self.running = False
291
+ console.print("\nGoodbye!")
292
+
293
+
294
+ def run_repl():
295
+ """Entry point for the REPL."""
296
+ repl = SecretsREPL()
297
+ repl.run()
@@ -0,0 +1,246 @@
1
+ """
2
+ Git-based secrets store for synchronization across machines.
3
+ """
4
+
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional
9
+
10
+ import click
11
+ from git import GitCommandError, Repo
12
+
13
+ from mcli.lib.logger.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+ from mcli.lib.ui.styling import error, info, success, warning
17
+
18
+
19
+ class SecretsStore:
20
+ """Manages git-based secrets synchronization."""
21
+
22
+ def __init__(self, store_path: Optional[Path] = None):
23
+ """Initialize the secrets store.
24
+
25
+ Args:
26
+ store_path: Path to git repository for secrets. Defaults to ~/repos/mcli-secrets
27
+ """
28
+ self.store_path = store_path or Path.home() / "repos" / "mcli-secrets"
29
+ self.config_file = Path.home() / ".mcli" / "secrets-store.conf"
30
+ self.load_config()
31
+
32
+ def load_config(self) -> None:
33
+ """Load store configuration."""
34
+ self.store_config = {}
35
+ if self.config_file.exists():
36
+ with open(self.config_file) as f:
37
+ for line in f:
38
+ line = line.strip()
39
+ if "=" in line:
40
+ key, value = line.split("=", 1)
41
+ self.store_config[key.strip()] = value.strip()
42
+
43
+ # Override store path if configured
44
+ if "store_path" in self.store_config:
45
+ self.store_path = Path(self.store_config["store_path"])
46
+
47
+ def save_config(self) -> None:
48
+ """Save store configuration."""
49
+ self.config_file.parent.mkdir(parents=True, exist_ok=True)
50
+ with open(self.config_file, "w") as f:
51
+ for key, value in self.store_config.items():
52
+ f.write(f"{key}={value}\n")
53
+
54
+ def init(self, remote_url: Optional[str] = None) -> None:
55
+ """Initialize the secrets store repository.
56
+
57
+ Args:
58
+ remote_url: Optional git remote URL
59
+ """
60
+ if self.store_path.exists() and (self.store_path / ".git").exists():
61
+ error("Store already initialized")
62
+ return
63
+
64
+ self.store_path.mkdir(parents=True, exist_ok=True)
65
+
66
+ try:
67
+ repo = Repo.init(self.store_path)
68
+
69
+ # Create README
70
+ readme_path = self.store_path / "README.md"
71
+ readme_path.write_text(
72
+ "# MCLI Secrets Store\n\n"
73
+ "This repository stores encrypted secrets for MCLI.\n\n"
74
+ "**WARNING**: This repository contains encrypted sensitive data.\n"
75
+ "Ensure it is kept private and access is restricted.\n"
76
+ )
77
+
78
+ # Create .gitignore
79
+ gitignore_path = self.store_path / ".gitignore"
80
+ gitignore_path.write_text("*.key\n*.tmp\n.DS_Store\n")
81
+
82
+ repo.index.add([readme_path.name, gitignore_path.name])
83
+ repo.index.commit("Initial commit")
84
+
85
+ if remote_url:
86
+ repo.create_remote("origin", remote_url)
87
+ self.store_config["remote_url"] = remote_url
88
+ self.save_config()
89
+ info(f"Remote added: {remote_url}")
90
+
91
+ success(f"Secrets store initialized at {self.store_path}")
92
+
93
+ except GitCommandError as e:
94
+ error(f"Failed to initialize store: {e}")
95
+
96
+ def push(self, secrets_dir: Path, message: Optional[str] = None) -> None:
97
+ """Push secrets to the store.
98
+
99
+ Args:
100
+ secrets_dir: Directory containing encrypted secrets
101
+ message: Commit message
102
+ """
103
+ if not self._check_initialized():
104
+ return
105
+
106
+ try:
107
+ repo = Repo(self.store_path)
108
+
109
+ # Copy secrets to store
110
+ store_secrets_dir = self.store_path / "secrets"
111
+
112
+ # Remove existing secrets
113
+ if store_secrets_dir.exists():
114
+ shutil.rmtree(store_secrets_dir)
115
+
116
+ # Copy new secrets
117
+ shutil.copytree(secrets_dir, store_secrets_dir)
118
+
119
+ # Add to git
120
+ repo.index.add(["secrets"])
121
+
122
+ # Check if there are changes
123
+ if repo.is_dirty():
124
+ message = message or f"Update secrets from {os.uname().nodename}"
125
+ repo.index.commit(message)
126
+
127
+ # Push if remote exists
128
+ if "origin" in repo.remotes:
129
+ info("Pushing to remote...")
130
+ repo.remotes.origin.push()
131
+ success("Secrets pushed to remote")
132
+ else:
133
+ success("Secrets committed locally")
134
+ else:
135
+ info("No changes to push")
136
+
137
+ except GitCommandError as e:
138
+ error(f"Failed to push secrets: {e}")
139
+
140
+ def pull(self, secrets_dir: Path) -> None:
141
+ """Pull secrets from the store.
142
+
143
+ Args:
144
+ secrets_dir: Directory to store pulled secrets
145
+ """
146
+ if not self._check_initialized():
147
+ return
148
+
149
+ try:
150
+ repo = Repo(self.store_path)
151
+
152
+ # Pull from remote if exists
153
+ if "origin" in repo.remotes:
154
+ info("Pulling from remote...")
155
+ repo.remotes.origin.pull()
156
+
157
+ # Copy secrets from store
158
+ store_secrets_dir = self.store_path / "secrets"
159
+
160
+ if not store_secrets_dir.exists():
161
+ warning("No secrets found in store")
162
+ return
163
+
164
+ # Backup existing secrets
165
+ if secrets_dir.exists():
166
+ backup_dir = secrets_dir.parent / f"{secrets_dir.name}.backup"
167
+ if backup_dir.exists():
168
+ shutil.rmtree(backup_dir)
169
+ shutil.move(str(secrets_dir), str(backup_dir))
170
+ info(f"Existing secrets backed up to {backup_dir}")
171
+
172
+ # Copy secrets from store
173
+ shutil.copytree(store_secrets_dir, secrets_dir)
174
+
175
+ success(f"Secrets pulled to {secrets_dir}")
176
+
177
+ except GitCommandError as e:
178
+ error(f"Failed to pull secrets: {e}")
179
+
180
+ def sync(self, secrets_dir: Path, message: Optional[str] = None) -> None:
181
+ """Synchronize secrets (pull then push).
182
+
183
+ Args:
184
+ secrets_dir: Directory containing secrets
185
+ message: Commit message
186
+ """
187
+ if not self._check_initialized():
188
+ return
189
+
190
+ info("Synchronizing secrets...")
191
+
192
+ # First pull
193
+ self.pull(secrets_dir)
194
+
195
+ # Then push
196
+ self.push(secrets_dir, message)
197
+
198
+ def status(self) -> Dict[str, Any]:
199
+ """Get status of the secrets store.
200
+
201
+ Returns:
202
+ Status information
203
+ """
204
+ status = {
205
+ "initialized": False,
206
+ "store_path": str(self.store_path),
207
+ "has_remote": False,
208
+ "remote_url": None,
209
+ "clean": True,
210
+ "branch": None,
211
+ "commit": None,
212
+ }
213
+
214
+ if not self._check_initialized(silent=True):
215
+ return status
216
+
217
+ try:
218
+ repo = Repo(self.store_path)
219
+ status["initialized"] = True
220
+ status["clean"] = not repo.is_dirty()
221
+ status["branch"] = repo.active_branch.name
222
+ status["commit"] = str(repo.head.commit)[:8]
223
+
224
+ if "origin" in repo.remotes:
225
+ status["has_remote"] = True
226
+ status["remote_url"] = repo.remotes.origin.url
227
+
228
+ except Exception:
229
+ pass
230
+
231
+ return status
232
+
233
+ def _check_initialized(self, silent: bool = False) -> bool:
234
+ """Check if store is initialized.
235
+
236
+ Args:
237
+ silent: Don't print error message
238
+
239
+ Returns:
240
+ True if initialized
241
+ """
242
+ if not self.store_path.exists() or not (self.store_path / ".git").exists():
243
+ if not silent:
244
+ error("Store not initialized. Run 'mcli secrets store init' first.")
245
+ return False
246
+ return True
mcli/self/self_cmd.py CHANGED
@@ -1029,6 +1029,14 @@ try:
1029
1029
  except ImportError as e:
1030
1030
  logger.debug(f"Could not load redis command: {e}")
1031
1031
 
1032
+ try:
1033
+ from mcli.self.zsh_cmd import zsh_group
1034
+
1035
+ self_app.add_command(zsh_group, name="zsh")
1036
+ logger.debug("Added zsh command to self group")
1037
+ except ImportError as e:
1038
+ logger.debug(f"Could not load zsh command: {e}")
1039
+
1032
1040
  try:
1033
1041
  from mcli.self.visual_cmd import visual
1034
1042
 
mcli/self/zsh_cmd.py ADDED
@@ -0,0 +1,259 @@
1
+ """
2
+ ZSH-specific commands and utilities for MCLI.
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import click
11
+
12
+ from mcli.lib.logger.logger import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+ from mcli.lib.ui.styling import error, info, success, warning
16
+
17
+
18
+ @click.group(name="zsh", help="ZSH shell integration and utilities")
19
+ def zsh_group():
20
+ """ZSH-specific commands and utilities."""
21
+ pass
22
+
23
+
24
+ @zsh_group.command(name="config", help="Configure ZSH for optimal MCLI experience")
25
+ @click.option("--force", is_flag=True, help="Force reconfiguration even if already set up")
26
+ def zsh_config(force: bool):
27
+ """Configure ZSH with MCLI-specific settings."""
28
+ zshrc = Path.home() / ".zshrc"
29
+
30
+ if not zshrc.exists():
31
+ if click.confirm("No .zshrc found. Create one?"):
32
+ zshrc.touch()
33
+ else:
34
+ warning("Configuration cancelled")
35
+ return
36
+
37
+ configs_added = [] # noqa: F841
38
+
39
+ # Read existing content
40
+ content = zshrc.read_text()
41
+
42
+ # MCLI configuration block
43
+ mcli_block_start = "# BEGIN MCLI ZSH CONFIG"
44
+ mcli_block_end = "# END MCLI ZSH CONFIG"
45
+
46
+ if mcli_block_start in content and not force:
47
+ info("MCLI ZSH configuration already exists. Use --force to reconfigure.")
48
+ return
49
+
50
+ # Remove old block if forcing
51
+ if mcli_block_start in content and force:
52
+ lines = content.split("\n")
53
+ new_lines = []
54
+ in_block = False
55
+
56
+ for line in lines:
57
+ if line.strip() == mcli_block_start:
58
+ in_block = True
59
+ elif line.strip() == mcli_block_end:
60
+ in_block = False
61
+ continue
62
+ elif not in_block:
63
+ new_lines.append(line)
64
+
65
+ content = "\n".join(new_lines)
66
+
67
+ # Build configuration
68
+ config_lines = [
69
+ "",
70
+ mcli_block_start,
71
+ "# MCLI aliases",
72
+ "alias m='mcli'",
73
+ "alias mc='mcli chat'",
74
+ "alias mw='mcli workflow'",
75
+ "alias ms='mcli self'",
76
+ "alias mls='mcli lib secrets'",
77
+ "alias mlsr='mcli lib secrets repl'",
78
+ "",
79
+ "# MCLI environment",
80
+ 'export MCLI_HOME="$HOME/.mcli"',
81
+ 'export PATH="$MCLI_HOME/bin:$PATH"',
82
+ "",
83
+ "# MCLI completion",
84
+ 'fpath=("$HOME/.config/zsh/completions" $fpath)',
85
+ "autoload -U compinit && compinit",
86
+ "",
87
+ "# MCLI prompt integration (optional)",
88
+ '# PS1="%{$fg[cyan]%}[mcli]%{$reset_color%} $PS1"',
89
+ "",
90
+ mcli_block_end,
91
+ "",
92
+ ]
93
+
94
+ # Append configuration
95
+ with zshrc.open("a") as f:
96
+ f.write("\n".join(config_lines))
97
+
98
+ success("ZSH configuration added successfully!")
99
+
100
+ # Install completion if not already installed
101
+ completion_dir = Path.home() / ".config" / "zsh" / "completions"
102
+ completion_file = completion_dir / "_mcli"
103
+
104
+ if not completion_file.exists():
105
+ info("Installing ZSH completion...")
106
+ try:
107
+ subprocess.run(["mcli", "self", "completion", "install", "--shell=zsh"], check=True)
108
+ configs_added.append("completion")
109
+ except subprocess.CalledProcessError:
110
+ warning("Failed to install completion automatically")
111
+
112
+ info("\nConfigured:")
113
+ info(" • Aliases: m, mc, mw, ms, mls, mlsr")
114
+ info(" • Environment variables: MCLI_HOME, PATH")
115
+ info(" • Shell completion support")
116
+ info("\nReload your shell configuration:")
117
+ info(" source ~/.zshrc")
118
+
119
+
120
+ @zsh_group.command(name="aliases", help="Show available ZSH aliases")
121
+ def zsh_aliases():
122
+ """Display MCLI ZSH aliases."""
123
+ aliases = [
124
+ ("m", "mcli", "Main MCLI command"),
125
+ ("mc", "mcli chat", "Open chat interface"),
126
+ ("mw", "mcli workflow", "Workflow commands"),
127
+ ("ms", "mcli self", "Self management commands"),
128
+ ("mls", "mcli lib secrets", "Secrets management"),
129
+ ("mlsr", "mcli lib secrets repl", "Secrets REPL"),
130
+ ]
131
+
132
+ info("MCLI ZSH Aliases:")
133
+ for alias, command, desc in aliases:
134
+ click.echo(f" {alias:<6} → {command:<25} # {desc}")
135
+
136
+
137
+ @zsh_group.command(name="prompt", help="Configure ZSH prompt with MCLI integration")
138
+ @click.option("--style", type=click.Choice(["simple", "powerline", "minimal"]), default="simple")
139
+ def zsh_prompt(style: str):
140
+ """Add MCLI status to ZSH prompt."""
141
+ zshrc = Path.home() / ".zshrc"
142
+
143
+ if not zshrc.exists():
144
+ error("No .zshrc found")
145
+ return
146
+
147
+ prompt_configs = {
148
+ "simple": 'PS1="%{$fg[cyan]%}[mcli]%{$reset_color%} $PS1"',
149
+ "powerline": 'PS1="%{$fg[cyan]%} mcli %{$reset_color%}$PS1"',
150
+ "minimal": 'PS1="◆ $PS1"',
151
+ }
152
+
153
+ config = prompt_configs[style]
154
+
155
+ # Check if prompt section exists
156
+ content = zshrc.read_text()
157
+ prompt_marker = "# MCLI prompt integration"
158
+
159
+ if prompt_marker in content:
160
+ # Update existing prompt
161
+ lines = content.split("\n")
162
+ for i, line in enumerate(lines):
163
+ if line.strip() == prompt_marker:
164
+ if i + 1 < len(lines) and lines[i + 1].startswith("PS1="):
165
+ lines[i + 1] = config
166
+ content = "\n".join(lines)
167
+ zshrc.write_text(content)
168
+ success(f"Updated prompt to {style} style")
169
+ break
170
+ else:
171
+ warning("MCLI ZSH configuration not found. Run 'mcli self zsh config' first.")
172
+
173
+
174
+ @zsh_group.command(name="functions", help="Install useful ZSH functions")
175
+ def zsh_functions():
176
+ """Install MCLI-specific ZSH functions."""
177
+ functions_dir = Path.home() / ".config" / "zsh" / "functions"
178
+ functions_dir.mkdir(parents=True, exist_ok=True)
179
+
180
+ # Create mcli-quick function
181
+ quick_func = functions_dir / "mcli-quick"
182
+ quick_func.write_text(
183
+ """# Quick MCLI command runner
184
+ mcli-quick() {
185
+ local cmd=$1
186
+ shift
187
+ mcli $cmd "$@" | head -20
188
+ }"""
189
+ )
190
+
191
+ # Create mcli-fzf function for fuzzy finding
192
+ fzf_func = functions_dir / "mcli-fzf"
193
+ fzf_func.write_text(
194
+ """# Fuzzy find MCLI commands
195
+ mcli-fzf() {
196
+ local cmd=$(mcli --help | grep -E '^ [a-z]' | awk '{print $1}' | fzf)
197
+ if [[ -n $cmd ]]; then
198
+ print -z "mcli $cmd "
199
+ fi
200
+ }"""
201
+ )
202
+
203
+ # Add to zshrc
204
+ zshrc = Path.home() / ".zshrc"
205
+ if zshrc.exists():
206
+ content = zshrc.read_text()
207
+ fpath_line = f'fpath=("{functions_dir}" $fpath)'
208
+
209
+ if str(functions_dir) not in content:
210
+ with zshrc.open("a") as f:
211
+ f.write(f"\n# MCLI ZSH functions\n{fpath_line}\nautoload -U mcli-quick mcli-fzf\n")
212
+
213
+ success("ZSH functions installed:")
214
+ info(" • mcli-quick: Run MCLI commands with truncated output")
215
+ info(" • mcli-fzf: Fuzzy find MCLI commands (requires fzf)")
216
+
217
+
218
+ @zsh_group.command(name="test", help="Test ZSH integration")
219
+ def zsh_test():
220
+ """Test ZSH integration and configuration."""
221
+ checks = []
222
+
223
+ # Check if running in ZSH
224
+ shell = os.environ.get("SHELL", "")
225
+ if "zsh" in shell:
226
+ checks.append(("ZSH shell detected", True))
227
+ else:
228
+ checks.append(("ZSH shell detected", False))
229
+
230
+ # Check completion
231
+ completion_file = Path.home() / ".config" / "zsh" / "completions" / "_mcli"
232
+ checks.append(("Completion installed", completion_file.exists()))
233
+
234
+ # Check zshrc
235
+ zshrc = Path.home() / ".zshrc"
236
+ if zshrc.exists():
237
+ content = zshrc.read_text()
238
+ checks.append(("MCLI config in .zshrc", "BEGIN MCLI ZSH CONFIG" in content))
239
+ checks.append(("Completion in fpath", ".config/zsh/completions" in content))
240
+ else:
241
+ checks.append((".zshrc exists", False))
242
+
243
+ # Check aliases
244
+ try:
245
+ result = subprocess.run(["zsh", "-c", "alias | grep mcli"], capture_output=True, text=True)
246
+ checks.append(("Aliases configured", result.returncode == 0))
247
+ except:
248
+ checks.append(("Aliases configured", False))
249
+
250
+ # Display results
251
+ info("ZSH Integration Test Results:")
252
+ for check, passed in checks:
253
+ status = "✅" if passed else "❌"
254
+ click.echo(f" {status} {check}")
255
+
256
+ if all(passed for _, passed in checks):
257
+ success("\nAll checks passed! ZSH integration is working correctly.")
258
+ else:
259
+ warning("\nSome checks failed. Run 'mcli self zsh config' to set up integration.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcli-framework
3
- Version: 7.8.5
3
+ Version: 7.9.0
4
4
  Summary: Portable workflow framework - transform any script into a versioned, schedulable command. Store in ~/.mcli/commands/, version with lockfile, run as daemon or cron job.
5
5
  Author-email: Luis Fernandez de la Vara <luis@lefv.io>
6
6
  Maintainer-email: Luis Fernandez de la Vara <luis@lefv.io>
@@ -48,6 +48,7 @@ Requires-Dist: humanize<5.0.0,>=4.9.0
48
48
  Requires-Dist: psutil<6.0.0,>=5.9.0
49
49
  Requires-Dist: inquirerpy<0.4.0,>=0.3.4
50
50
  Requires-Dist: gitpython<4.0.0,>=3.1.40
51
+ Requires-Dist: prompt-toolkit<4.0.0,>=3.0.0
51
52
  Requires-Dist: aiohttp>=3.9.0
52
53
  Requires-Dist: httpx>=0.28.1
53
54
  Requires-Dist: websockets>=12.0
@@ -5,7 +5,7 @@ mcli/config.toml,sha256=263yEVvP_W9F2zOLssUBgy7amKaRAFQuBrfxcMhKxaQ,1706
5
5
  mcli/app/__init__.py,sha256=D4RiKk2gOEXwanbe_jXyNSb5zdgNi47kahtskMnEwjY,489
6
6
  mcli/app/commands_cmd.py,sha256=gixyyrMV4wDLeTRRDlUx1NG46ceIswsIV3u-zPvtNjw,58655
7
7
  mcli/app/completion_helpers.py,sha256=e62C6w2N-XoD66GYYHgtvKKoD3kYMuIeBBGzVKbuL04,7497
8
- mcli/app/main.py,sha256=3ehRwx_-9M4yOa--CTsRq9EKyN1UukRGKXHSWNmul7I,18944
8
+ mcli/app/main.py,sha256=aFQbKMTqClswmwwxpbS5zxVXOXcZMvF27LO19x1X7Cg,19208
9
9
  mcli/app/model_cmd.py,sha256=OkFxJwZFCO-8IH6j1FPq-32qqhitbDvrUGf3IooBL54,2562
10
10
  mcli/app/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  mcli/app/model/model.py,sha256=EUGu_td-hRlbf4OElkdk1-0p7WyuG7sZmb-Ux2-J9KY,39061
@@ -19,7 +19,7 @@ mcli/chat/system_controller.py,sha256=SuGvnIh2QObvM1DMicF3gGyeBkbz_xXS-hOOHjWx5j
19
19
  mcli/chat/system_integration.py,sha256=xQ11thOUswPg8r1HZkId6U3bTCOtMYngt0-mUYYXpt4,40196
20
20
  mcli/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  mcli/lib/custom_commands.py,sha256=PCC3uRLN6bcIKCh7GJ98vuNm5R2o9xRRCvCMQcIhj0o,14489
22
- mcli/lib/lib.py,sha256=mlp2INx-UKTOECcA7Kens9yNt2gJi7GbKWFmf4cxj0c,632
22
+ mcli/lib/lib.py,sha256=-CFUfmcubYBxt3LDBY0uj9DF232pz8MPDu-Qg0Ocy8M,850
23
23
  mcli/lib/paths.py,sha256=k6sDwvD8QRzBkBOllvXkokameumpTjpJ7pQrP7z1en0,2455
24
24
  mcli/lib/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  mcli/lib/api/api.py,sha256=sPgAIYC8Z7AWV2TCBssNSKotbRggBqNLsbfzbjkhmUY,18558
@@ -57,6 +57,11 @@ mcli/lib/performance/uvloop_config.py,sha256=wyI5pQnec2RAhgm52HJ1AxYGFa3bjTa-Cjh
57
57
  mcli/lib/pickles/__init__.py,sha256=b7l9CLv8Aua5sROYAaA4raxpWTKSJxUumTLqmzlkb-I,33
58
58
  mcli/lib/pickles/pickles.py,sha256=O9dLJfyxViX-IyionbcjcsxHnq42XiLaAorsUrx9oZU,1448
59
59
  mcli/lib/search/cached_vectorizer.py,sha256=ShBSTqoyGNGTNAII34okxl4chzy7hqhO2R-jtTEF1_0,17996
60
+ mcli/lib/secrets/__init__.py,sha256=amoAq00syRdQuII3BnvizGmhXbf6vZNN2qnrHtBROvA,241
61
+ mcli/lib/secrets/commands.py,sha256=-PovQ4X1QV3LxqO3hRBIG8JkqA0SJlA_skcg-xCxg-M,6169
62
+ mcli/lib/secrets/manager.py,sha256=kjHNQ7_TeQlF1ZyfhfKZ00Mu1wn06Vw6K8FQL0nyU1s,6698
63
+ mcli/lib/secrets/repl.py,sha256=rmBFVi8lB4r3pRnicpX318IaTrtshPEaBqC-PZaMs0Y,10351
64
+ mcli/lib/secrets/store.py,sha256=vl-K_OjqlNIvypf4BCwCbtNZPk91QngNYqhwWwKupDw,7838
60
65
  mcli/lib/services/data_pipeline.py,sha256=UwDpAByOL_PDMjD76aOTmCyStd_QBmCngZBMXVerR5Y,16475
61
66
  mcli/lib/services/lsh_client.py,sha256=sJSXlWBqnhNQy7TtYMmcAwBceUp45rYa_HgdyYF0WtI,16799
62
67
  mcli/lib/services/redis_service.py,sha256=5QwSB-FMIS1zdTNp8VSOrZfr_wrUK10Bfe2N1ZTy-90,12730
@@ -191,10 +196,11 @@ mcli/self/__init__.py,sha256=7hCrgaRb4oAgAf-kcyzqhJ5LFpW19jwF5bxowR4LwjY,41
191
196
  mcli/self/completion_cmd.py,sha256=FKNVc_4ikWTGbDHybiNZGdxrggvt6A6q1rnzuyFVzVM,7754
192
197
  mcli/self/logs_cmd.py,sha256=SCzZ4VZs6p42hksun_w4WN33xIZgmq7RjdWX8P2WcT4,15056
193
198
  mcli/self/redis_cmd.py,sha256=Cl0LQ3Mqt27gLeb542_xw6bJBbIE-CBmWyMmaUTSk8c,9426
194
- mcli/self/self_cmd.py,sha256=IhuBxXNvWuUl9gggAW-bLfphA_MJTQ6HDTKjRkGbSEI,37531
199
+ mcli/self/self_cmd.py,sha256=531_8jfX6neSifSl_u3mCG_EcSkQJ9yXcFxklvMFea0,37760
195
200
  mcli/self/store_cmd.py,sha256=O6arjRr4qWQKh1QyVWtzyXq5R7yZEBL87FSI59Db7IY,13320
196
201
  mcli/self/test_cmd.py,sha256=WjzgoH1WFa79wc8A7O6UMuJfookLfgciUNcCMbKHAQQ,21
197
202
  mcli/self/visual_cmd.py,sha256=jXighahHxeM9HANQ2Brk6nKFgi2ZuQBOBH7PE5xhebk,9428
203
+ mcli/self/zsh_cmd.py,sha256=63jKmfjhJp2zxJL2c37OdtdzDrnOreXXfyARN7TyfzU,8294
198
204
  mcli/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
199
205
  mcli/workflow/lsh_integration.py,sha256=jop80DUjdOSxmqPb-gX_OBep5f1twViv-pXmkcFqBPY,13314
200
206
  mcli/workflow/workflow.py,sha256=P_W5LOB3lowvvlfEp3mGwS3eNq4tpbiUY-poFulAF9E,393
@@ -268,9 +274,9 @@ mcli/workflow/sync/test_cmd.py,sha256=neVgs9zEnKSxlvzDpFkuCGucqnzjrShm2OvJtHibsl
268
274
  mcli/workflow/videos/__init__.py,sha256=aV3DEoO7qdKJY4odWKoQbOKDQq4ludTeCLnZcupOFIM,25
269
275
  mcli/workflow/wakatime/__init__.py,sha256=wKG8cVIHVtMPhNRFGFtX43bRnocHqOMMkFMkmW-M6pU,2626
270
276
  mcli/workflow/wakatime/wakatime.py,sha256=sEjsUKa3-XyE8Ni6sAb_D3GAY5jDcA30KknW9YTbLTA,142
271
- mcli_framework-7.8.5.dist-info/licenses/LICENSE,sha256=sahwAMfrJv2-V66HNPTp7A9UmMjxtyejwTZZoWQvEcI,1075
272
- mcli_framework-7.8.5.dist-info/METADATA,sha256=0cVb6nBeHAaVhwARz0Up36ixqhdfliwfLR3cVAVtTC8,16374
273
- mcli_framework-7.8.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
274
- mcli_framework-7.8.5.dist-info/entry_points.txt,sha256=dYrZbDIm-KUPsl1wfv600Kx_8sMy89phMkCihbDRgP8,261
275
- mcli_framework-7.8.5.dist-info/top_level.txt,sha256=_bnO8J2EUkliWivey_1le0UrnocFKmyVMQjbQ8iVXjc,5
276
- mcli_framework-7.8.5.dist-info/RECORD,,
277
+ mcli_framework-7.9.0.dist-info/licenses/LICENSE,sha256=sahwAMfrJv2-V66HNPTp7A9UmMjxtyejwTZZoWQvEcI,1075
278
+ mcli_framework-7.9.0.dist-info/METADATA,sha256=JNzXPY0C0wGjCEu8B8DR9r7EiFYlYQ0_DBNFAHrcgRk,16418
279
+ mcli_framework-7.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
280
+ mcli_framework-7.9.0.dist-info/entry_points.txt,sha256=dYrZbDIm-KUPsl1wfv600Kx_8sMy89phMkCihbDRgP8,261
281
+ mcli_framework-7.9.0.dist-info/top_level.txt,sha256=_bnO8J2EUkliWivey_1le0UrnocFKmyVMQjbQ8iVXjc,5
282
+ mcli_framework-7.9.0.dist-info/RECORD,,