shellmate-cli 3.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.
Files changed (106) hide show
  1. shellmate/__init__.py +0 -0
  2. shellmate/adapters/__init__.py +67 -0
  3. shellmate/adapters/bash_adapter.sh +7 -0
  4. shellmate/adapters/ble_adapter.sh +350 -0
  5. shellmate/adapters/powershell_adapter.ps1 +417 -0
  6. shellmate/adapters/powershell_predictor.ps1 +97 -0
  7. shellmate/adapters/zsh_adapter.zsh +8 -0
  8. shellmate/agents/__init__.py +23 -0
  9. shellmate/agents/base.py +248 -0
  10. shellmate/agents/chain_patterns.yaml +98 -0
  11. shellmate/agents/command_chain.py +322 -0
  12. shellmate/agents/context_gatherer.py +175 -0
  13. shellmate/agents/environment.py +363 -0
  14. shellmate/agents/librarian.py +502 -0
  15. shellmate/agents/query.py +415 -0
  16. shellmate/agents/repomix_backend.py +171 -0
  17. shellmate/agents/watcher.py +266 -0
  18. shellmate/cache/__init__.py +0 -0
  19. shellmate/cache/semantic_cache.py +41 -0
  20. shellmate/cli/__init__.py +0 -0
  21. shellmate/cli/commands/__init__.py +27 -0
  22. shellmate/cli/commands/analyze.py +5 -0
  23. shellmate/cli/commands/config.py +319 -0
  24. shellmate/cli/commands/configure.py +118 -0
  25. shellmate/cli/commands/disable.py +11 -0
  26. shellmate/cli/commands/enable.py +49 -0
  27. shellmate/cli/commands/init.py +412 -0
  28. shellmate/cli/commands/install.py +242 -0
  29. shellmate/cli/commands/migrate.py +147 -0
  30. shellmate/cli/commands/status.py +31 -0
  31. shellmate/cli/commands/uninstall.py +78 -0
  32. shellmate/cli/main.py +114 -0
  33. shellmate/cli/parser.py +184 -0
  34. shellmate/cli/version.py +1 -0
  35. shellmate/completion/__init__.py +4 -0
  36. shellmate/completion/bash_adapter.py +145 -0
  37. shellmate/config/__init__.py +33 -0
  38. shellmate/config/credentials.py +120 -0
  39. shellmate/config/defaults.py +88 -0
  40. shellmate/config/manager.py +94 -0
  41. shellmate/config/paths.py +77 -0
  42. shellmate/config/secrets.py +66 -0
  43. shellmate/daemon/__init__.py +0 -0
  44. shellmate/daemon/lifecycle.py +308 -0
  45. shellmate/daemon/observability.py +82 -0
  46. shellmate/daemon/pipeline.py +119 -0
  47. shellmate/daemon/server.py +605 -0
  48. shellmate/daemon/session.py +170 -0
  49. shellmate/daemon/startup_ui.py +43 -0
  50. shellmate/environment/__init__.py +0 -0
  51. shellmate/environment/detector.py +130 -0
  52. shellmate/ipc/__init__.py +0 -0
  53. shellmate/ipc/client.py +210 -0
  54. shellmate/ipc/ps_client.py +190 -0
  55. shellmate/orchestration/__init__.py +3 -0
  56. shellmate/orchestration/analysis_graph.py +1274 -0
  57. shellmate/orchestration/state.py +35 -0
  58. shellmate/parser/__init__.py +0 -0
  59. shellmate/parser/command_parser.py +111 -0
  60. shellmate/plugins/__init__.py +0 -0
  61. shellmate/plugins/base.py +80 -0
  62. shellmate/plugins/loader.py +41 -0
  63. shellmate/providers/__init__.py +0 -0
  64. shellmate/providers/base.py +97 -0
  65. shellmate/providers/groq_command_guide.py +108 -0
  66. shellmate/providers/groq_provider.py +127 -0
  67. shellmate/providers/groq_supervisor.py +78 -0
  68. shellmate/providers/openrouter_provider.py +149 -0
  69. shellmate/providers/router.py +272 -0
  70. shellmate/providers/validate_keys.py +98 -0
  71. shellmate/providers/xai_provider.py +142 -0
  72. shellmate/rules/__init__.py +0 -0
  73. shellmate/rules/catastrophic.yaml +96 -0
  74. shellmate/rules/catastrophic_matcher.py +167 -0
  75. shellmate/rules/definitions/__init__.py +0 -0
  76. shellmate/rules/definitions/file_deletion.yaml +54 -0
  77. shellmate/rules/definitions/filesystem.yaml +43 -0
  78. shellmate/rules/definitions/network_execution.yaml +46 -0
  79. shellmate/rules/definitions/permissions.yaml +54 -0
  80. shellmate/rules/definitions/process_control.yaml +37 -0
  81. shellmate/rules/definitions/system_destruction.yaml +67 -0
  82. shellmate/rules/engine.py +200 -0
  83. shellmate/sanitizer/__init__.py +0 -0
  84. shellmate/sanitizer/typo_detector.py +114 -0
  85. shellmate/ui/__init__.py +10 -0
  86. shellmate/ui/analysis.py +164 -0
  87. shellmate/ui/annotations.py +232 -0
  88. shellmate/ui/banner.py +49 -0
  89. shellmate/ui/progress.py +126 -0
  90. shellmate/ui/runtime/__init__.py +15 -0
  91. shellmate/ui/runtime/console_factory.py +65 -0
  92. shellmate/ui/runtime/live_scene.py +116 -0
  93. shellmate/ui/runtime/motion_policy.py +69 -0
  94. shellmate/ui/runtime/tokens.py +47 -0
  95. shellmate/ui/scenes/__init__.py +24 -0
  96. shellmate/ui/scenes/enable.py +316 -0
  97. shellmate/ui/scenes/help.py +53 -0
  98. shellmate/ui/scenes/stages.py +128 -0
  99. shellmate/ui/scenes/status.py +181 -0
  100. shellmate/ui/status.py +3 -0
  101. shellmate_cli-3.0.0.dist-info/METADATA +352 -0
  102. shellmate_cli-3.0.0.dist-info/RECORD +106 -0
  103. shellmate_cli-3.0.0.dist-info/WHEEL +5 -0
  104. shellmate_cli-3.0.0.dist-info/entry_points.txt +3 -0
  105. shellmate_cli-3.0.0.dist-info/licenses/LICENSE +21 -0
  106. shellmate_cli-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,184 @@
1
+ import sys
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional, List, Dict
5
+ from enum import Enum
6
+
7
+
8
+ class ExitCode(Enum):
9
+ SUCCESS = 0
10
+ USAGE_ERROR = 2
11
+ TEMP_ERROR = 3
12
+ INTERNAL_ERROR = 4
13
+
14
+
15
+ class Action(Enum):
16
+ ENABLE = "enable"
17
+ DISABLE = "disable"
18
+ STATUS = "status"
19
+ CONFIG = "config"
20
+ INSTALL = "install"
21
+ UNINSTALL = "uninstall"
22
+ HELP = "help"
23
+ VERSION = "version"
24
+
25
+
26
+ @dataclass
27
+ class ParsedCommand:
28
+ action: Action
29
+ raw_command: Optional[str] = None
30
+ tokens: List[str] = None
31
+ error: Optional[str] = None
32
+ suggestion: Optional[str] = None
33
+ exit_code: ExitCode = ExitCode.SUCCESS
34
+ quiet: bool = False
35
+ no_color: bool = False
36
+ dry_run: bool = False
37
+ verbose: bool = False # NEW Phase 5 - Task 5.1
38
+
39
+ # NEW v3 fields
40
+ queries: List[str] = field(default_factory=list)
41
+ directives: List[str] = field(default_factory=list)
42
+ overrides: Dict[str, str] = field(default_factory=dict)
43
+
44
+ def __post_init__(self):
45
+ if self.tokens is None:
46
+ self.tokens = []
47
+
48
+
49
+ _SUBCOMMANDS = frozenset({
50
+ "enable", "disable", "status", "config",
51
+ "install", "uninstall",
52
+ "-h", "--help", "-v", "--version",
53
+ })
54
+
55
+
56
+ def _extract_bracket_syntax(command: str) -> tuple:
57
+ """Extract [?...], [!...], [@...] from command string.
58
+
59
+ Returns:
60
+ tuple: (cleaned_command, queries, directives, overrides)
61
+ """
62
+ # Extract queries [?...]
63
+ queries = re.findall(r'\[\?([^\]]+)\]', command)
64
+
65
+ # Extract directives [!...]
66
+ directives = re.findall(r'\[!([^\]]+)\]', command)
67
+
68
+ # Extract overrides [@...]
69
+ overrides_raw = re.findall(r'\[@([^\]]+)\]', command)
70
+ overrides = {}
71
+ for override in overrides_raw:
72
+ if '=' in override:
73
+ key, value = override.split('=', 1)
74
+ overrides[key.strip()] = value.strip()
75
+
76
+ # Remove all brackets from command
77
+ cleaned = re.sub(r'\[[\?!@][^\]]+\]', '', command).strip()
78
+
79
+ return cleaned, queries, directives, overrides
80
+
81
+
82
+ class ShellMateCommandParser:
83
+ def __init__(self, argv: List[str] = None):
84
+ self.argv = argv or sys.argv[1:]
85
+
86
+ def _consume_global_flags(self, args: List[str]):
87
+ """Parse sm-level flags only before the subcommand.
88
+
89
+ Flags that belong to the analyzed shell command (e.g. pytest -q) must
90
+ not be stripped from argv.
91
+ """
92
+ quiet = False
93
+ no_color = False
94
+ dry_run = False
95
+ verbose = False # NEW Phase 5 - Task 5.1
96
+ remaining = list(args)
97
+
98
+ while remaining and remaining[0] not in _SUBCOMMANDS:
99
+ flag = remaining.pop(0)
100
+ if flag in ("--quiet", "-q"):
101
+ quiet = True
102
+ elif flag == "--no-color":
103
+ no_color = True
104
+ elif flag == "--dry-run":
105
+ dry_run = True
106
+ elif flag == "--verbose":
107
+ verbose = True
108
+ else:
109
+ remaining.insert(0, flag)
110
+ break
111
+
112
+ return remaining, quiet, no_color, dry_run, verbose
113
+
114
+ def parse(self) -> ParsedCommand:
115
+ # Bare `sm` (and flag-only argv) → usage help (Done When: sm shows command list)
116
+ if not self.argv:
117
+ return ParsedCommand(action=Action.HELP)
118
+
119
+ args, quiet, no_color, dry_run, verbose = self._consume_global_flags(self.argv)
120
+ self._dry_run = dry_run
121
+
122
+ if not args:
123
+ return ParsedCommand(
124
+ action=Action.HELP,
125
+ no_color=no_color,
126
+ dry_run=self._dry_run,
127
+ verbose=verbose,
128
+ )
129
+
130
+ first_arg = args[0]
131
+
132
+ if first_arg in ("-h", "--help"):
133
+ return ParsedCommand(action=Action.HELP, no_color=no_color, dry_run=self._dry_run, verbose=verbose)
134
+
135
+ if first_arg in ("-v", "--version"):
136
+ return ParsedCommand(action=Action.VERSION, no_color=no_color, dry_run=self._dry_run, verbose=verbose)
137
+
138
+ if first_arg in ("enable", "disable", "status", "config", "install", "uninstall"):
139
+ parsed = self._parse_simple_command(first_arg, args[1:])
140
+ parsed.no_color = no_color
141
+ parsed.dry_run = self._dry_run
142
+ parsed.verbose = verbose
143
+ return parsed
144
+
145
+ if first_arg.endswith(".py") or "test_" in first_arg or "/test" in first_arg or "\\test" in first_arg or "tests/" in first_arg:
146
+ return ParsedCommand(
147
+ action=Action.HELP,
148
+ error="No command provided",
149
+ suggestion="Run 'sm --help' for usage",
150
+ exit_code=ExitCode.USAGE_ERROR,
151
+ no_color=no_color,
152
+ dry_run=self._dry_run,
153
+ verbose=verbose,
154
+ )
155
+
156
+ return ParsedCommand(
157
+ action=Action.HELP,
158
+ error=f"Unknown command: {first_arg}",
159
+ suggestion="Run 'sm --help' for usage.",
160
+ exit_code=ExitCode.USAGE_ERROR,
161
+ no_color=no_color,
162
+ dry_run=self._dry_run,
163
+ verbose=verbose,
164
+ )
165
+
166
+ def _parse_simple_command(self, cmd: str, rest: Optional[List[str]] = None) -> ParsedCommand:
167
+ action_map = {
168
+ "enable": Action.ENABLE,
169
+ "disable": Action.DISABLE,
170
+ "status": Action.STATUS,
171
+ "config": Action.CONFIG,
172
+ "install": Action.INSTALL,
173
+ "uninstall": Action.UNINSTALL,
174
+ }
175
+ parsed = ParsedCommand(action=action_map[cmd])
176
+ # Stash remaining flags for commands that accept them
177
+ if rest:
178
+ parsed.tokens = list(rest)
179
+ return parsed
180
+
181
+
182
+ def parse_argv(argv: List[str] = None) -> ParsedCommand:
183
+ parser = ShellMateCommandParser(argv)
184
+ return parser.parse()
@@ -0,0 +1 @@
1
+ VERSION = "3.0.0"
@@ -0,0 +1,4 @@
1
+ """Completion source adapters for autocomplete mode"""
2
+ from shellmate.completion.bash_adapter import get_completions
3
+
4
+ __all__ = ["get_completions"]
@@ -0,0 +1,145 @@
1
+ """Completion source adapters for autocomplete mode (Phase 4).
2
+
3
+ Works offline / without bash (Windows) via static subcommand maps,
4
+ then bash compgen, then carapace.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import shlex
10
+ import subprocess
11
+ from typing import List
12
+
13
+ # Shared with ipc/ps_client offline path — keep in sync for ghost-text / Tab.
14
+ _SUBCOMMANDS = {
15
+ "git": [
16
+ "status", "push", "pull", "commit", "checkout", "branch", "log", "diff",
17
+ "add", "merge", "rebase", "reset", "fetch", "clone", "stash", "remote",
18
+ ],
19
+ "docker": ["ps", "images", "run", "build", "exec", "stop", "rm", "compose", "logs"],
20
+ "kubectl": ["get", "apply", "delete", "describe", "logs", "exec", "port-forward"],
21
+ "npm": ["install", "run", "test", "build", "start", "ci"],
22
+ }
23
+
24
+ _FLAG_MAP = {
25
+ "git": ["--force", "--all", "--verbose", "--help", "--amend", "--no-verify"],
26
+ "docker": ["--help", "--version", "--verbose", "--rm", "-it"],
27
+ "kubectl": ["--namespace", "--context", "--kubeconfig", "--help"],
28
+ }
29
+
30
+
31
+ def get_completions(partial_command: str) -> List[str]:
32
+ """
33
+ Get completions for a partial command.
34
+
35
+ Order: static map (always available) → bash compgen → carapace.
36
+ Returns full-line suggestions when completing a subcommand (e.g. "git push")
37
+ so ble.sh ghost text can strip the typed prefix.
38
+ """
39
+ candidates = get_completions_static(partial_command)
40
+ if candidates:
41
+ return candidates
42
+
43
+ candidates = get_completions_bash(partial_command)
44
+ if candidates:
45
+ return candidates
46
+
47
+ return get_completions_carapace(partial_command)
48
+
49
+
50
+ def get_completions_static(partial_command: str) -> List[str]:
51
+ """Offline completions from known program maps (Windows-safe)."""
52
+ partial = (partial_command or "").strip()
53
+ if not partial:
54
+ return []
55
+
56
+ try:
57
+ tokens = shlex.split(partial_command)
58
+ except ValueError:
59
+ tokens = partial_command.split()
60
+
61
+ if not tokens:
62
+ return []
63
+
64
+ program = tokens[0]
65
+ ends_with_space = partial_command.endswith(" ")
66
+ current = "" if ends_with_space else tokens[-1]
67
+
68
+ # Flag completion
69
+ if current.startswith("-"):
70
+ flags = get_flag_completions(program, current)
71
+ return flags[:10]
72
+
73
+ # Program name completion (single token)
74
+ if len(tokens) == 1 and not ends_with_space:
75
+ return [p for p in _SUBCOMMANDS if p.startswith(program)][:10]
76
+
77
+ # Subcommand completion: "git pu" → ["git push", "git pull"]
78
+ if program in _SUBCOMMANDS:
79
+ if len(tokens) == 1 and ends_with_space:
80
+ prefix = ""
81
+ elif len(tokens) == 2 and not ends_with_space:
82
+ prefix = tokens[1]
83
+ else:
84
+ return []
85
+
86
+ matches = [s for s in _SUBCOMMANDS[program] if s.startswith(prefix)]
87
+ # Full-line form for adapters; also expose bare subcommands for callers
88
+ # that expect pivot-style ["push", "pull"].
89
+ full = [f"{program} {m}" for m in matches]
90
+ return (full or matches)[:10]
91
+
92
+ return []
93
+
94
+
95
+ def get_completions_bash(partial_command: str) -> List[str]:
96
+ """Get bash completions using compgen (Linux/WSL/Git Bash)."""
97
+ try:
98
+ tokens = shlex.split(partial_command)
99
+ if not tokens:
100
+ return []
101
+
102
+ program = tokens[0]
103
+ current = tokens[-1] if len(tokens) > 1 else program
104
+
105
+ # Escape for safe shell interpolation
106
+ safe_current = current.replace("'", "'\\''")
107
+ result = subprocess.run(
108
+ ["bash", "-c", f"compgen -c -- '{safe_current}'"],
109
+ capture_output=True,
110
+ text=True,
111
+ timeout=0.1,
112
+ )
113
+
114
+ if result.returncode == 0 and result.stdout.strip():
115
+ candidates = [c for c in result.stdout.strip().split("\n") if c]
116
+ # Prefer candidates that extend the typed token
117
+ filtered = [c for c in candidates if c.startswith(current)][:10]
118
+ return filtered
119
+
120
+ return []
121
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
122
+ return []
123
+
124
+
125
+ def get_flag_completions(program: str, partial_flag: str) -> List[str]:
126
+ """Get flag completions for known programs."""
127
+ flags = _FLAG_MAP.get(program, [])
128
+ return [f for f in flags if f.startswith(partial_flag)]
129
+
130
+
131
+ def get_completions_carapace(partial_command: str) -> List[str]:
132
+ """Fallback to carapace if bash-completion unavailable."""
133
+ try:
134
+ result = subprocess.run(
135
+ ["carapace", "_carapace", "bash", partial_command],
136
+ capture_output=True,
137
+ text=True,
138
+ timeout=0.2,
139
+ )
140
+
141
+ if result.returncode == 0 and result.stdout.strip():
142
+ return [c for c in result.stdout.strip().split("\n") if c][:10]
143
+ return []
144
+ except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
145
+ return []
@@ -0,0 +1,33 @@
1
+ from shellmate.config.defaults import get_default_config, SHELLMATE_DIR
2
+ from shellmate.config.credentials import (
3
+ CREDENTIAL_KEYS,
4
+ AI_CREDENTIAL_KEYS,
5
+ load_env,
6
+ save_env,
7
+ get_credential_status,
8
+ get_api_key,
9
+ export_script,
10
+ )
11
+ from shellmate.config.manager import get_config, get_config_value, set_config_value, get_config_manager
12
+ from shellmate.config.secrets import redact_secrets, redact_dict, sanitize_for_logging, sanitize_prompt, sanitize_response
13
+
14
+ __all__ = [
15
+ "get_default_config",
16
+ "SHELLMATE_DIR",
17
+ "CREDENTIAL_KEYS",
18
+ "AI_CREDENTIAL_KEYS",
19
+ "load_env",
20
+ "save_env",
21
+ "get_credential_status",
22
+ "get_api_key",
23
+ "export_script",
24
+ "get_config",
25
+ "get_config_value",
26
+ "set_config_value",
27
+ "get_config_manager",
28
+ "redact_secrets",
29
+ "redact_dict",
30
+ "sanitize_for_logging",
31
+ "sanitize_prompt",
32
+ "sanitize_response",
33
+ ]
@@ -0,0 +1,120 @@
1
+ import os
2
+ import stat
3
+ from pathlib import Path
4
+ from shellmate.config.paths import CREDENTIALS_FILE, ensure_dirs
5
+
6
+
7
+ CREDENTIAL_KEYS = {
8
+ "GROQ_API_KEY": {
9
+ "label": "Groq API Key",
10
+ "description": "For AI risk analysis (get at https://console.groq.com/keys)",
11
+ "doc_url": "https://console.groq.com/keys",
12
+ "required": False,
13
+ "provider": "groq",
14
+ "live_check": True,
15
+ },
16
+ "OPENROUTER_API_KEY": {
17
+ "label": "OpenRouter API Key",
18
+ "description": "For AI remediation suggestions (get at https://openrouter.ai/keys)",
19
+ "doc_url": "https://openrouter.ai/keys",
20
+ "required": False,
21
+ "provider": "openrouter",
22
+ "live_check": True,
23
+ },
24
+ "XAI_API_KEY": {
25
+ "label": "Grok (xAI) API Key",
26
+ "description": "For AI risk analysis and remediation (get at https://console.x.ai/)",
27
+ "doc_url": "https://console.x.ai/",
28
+ "required": False,
29
+ "provider": "xai",
30
+ "live_check": True,
31
+ },
32
+ "CONTEXT7_API_KEY": {
33
+ "label": "Context7 API Key",
34
+ "description": "For documentation lookup (get at https://context7.com)",
35
+ "doc_url": "https://context7.com",
36
+ "required": False,
37
+ "provider": "context7",
38
+ "live_check": False,
39
+ },
40
+ }
41
+
42
+ # AI providers that can power risk/remediation (excludes optional Context7)
43
+ AI_CREDENTIAL_KEYS = ("GROQ_API_KEY", "OPENROUTER_API_KEY", "XAI_API_KEY")
44
+
45
+
46
+ def load_env() -> dict:
47
+ if not CREDENTIALS_FILE.exists():
48
+ return {}
49
+ env = {}
50
+ try:
51
+ with open(CREDENTIALS_FILE, "r") as f:
52
+ for line in f:
53
+ line = line.strip()
54
+ if not line or line.startswith("#"):
55
+ continue
56
+ if "=" in line:
57
+ key, _, value = line.partition("=")
58
+ key = key.strip()
59
+ value = value.strip().strip("\"'")
60
+ env[key] = value
61
+ except OSError:
62
+ pass
63
+ return env
64
+
65
+
66
+ def save_env(env_dict: dict) -> None:
67
+ ensure_dirs()
68
+ with open(CREDENTIALS_FILE, "w") as f:
69
+ f.write("# ShellMate Credentials\n")
70
+ f.write("# Generated by `sm config`\n")
71
+ f.write("# Add your API keys below (one per line, KEY=VALUE format)\n\n")
72
+ for key in CREDENTIAL_KEYS:
73
+ value = env_dict.get(key, "")
74
+ if value:
75
+ f.write(f"{key}={value}\n")
76
+ else:
77
+ f.write(f"# {key}=\n")
78
+
79
+ if os.name != "nt":
80
+ try:
81
+ CREDENTIALS_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
82
+ except OSError:
83
+ pass
84
+
85
+
86
+ def get_credential_status() -> dict:
87
+ env = load_env()
88
+ result = {}
89
+ for key, info in CREDENTIAL_KEYS.items():
90
+ value = env.get(key, os.environ.get(key, ""))
91
+ result[key] = {
92
+ "configured": bool(value),
93
+ "label": info["label"],
94
+ "masked": _mask_key(value) if value else None,
95
+ }
96
+ return result
97
+
98
+
99
+ def get_api_key(key_name: str) -> str:
100
+ env = load_env()
101
+ return os.environ.get(key_name, "") or env.get(key_name, "")
102
+
103
+
104
+ def _mask_key(key: str) -> str:
105
+ if len(key) <= 8:
106
+ return key[:4] + ("*" * (len(key) - 4))
107
+ return key[:4] + ("*" * (len(key) - 8)) + key[-4:]
108
+
109
+
110
+ def export_script(shell: str = "bash") -> str:
111
+ env = load_env()
112
+ lines = []
113
+ for key in CREDENTIAL_KEYS:
114
+ value = env.get(key)
115
+ if value:
116
+ if shell == "powershell":
117
+ lines.append(f"$env:{key} = '{value}'")
118
+ else:
119
+ lines.append(f"export {key}='{value}'")
120
+ return "\n".join(lines)
@@ -0,0 +1,88 @@
1
+ import os
2
+ from shellmate.config.paths import get_cache_dir, get_log_dir, get_session_dir, get_state_dir
3
+
4
+ SHELLMATE_DIR = str(get_state_dir())
5
+
6
+
7
+ def get_default_config():
8
+ return {
9
+ "daemon": {
10
+ "host": "127.0.0.1",
11
+ "port": 9823,
12
+ "socket_path": str(get_session_dir() / "shellmate.sock"),
13
+ "pipe_name": r"\\.\pipe\shellmate",
14
+ },
15
+ "cache": {
16
+ "enabled": True,
17
+ "backend": "faiss",
18
+ "model": "all-MiniLM-L6-v2",
19
+ "similarity_threshold": 0.92,
20
+ "expiration_days": 30,
21
+ "max_entries": 10000,
22
+ "directory": str(get_cache_dir()),
23
+ },
24
+ "providers": {
25
+ "risk_analysis": {
26
+ "primary": "groq",
27
+ "model": "qwen/qwen3.8-27b",
28
+ "fallback": None,
29
+ },
30
+ "remediation": {
31
+ "primary": "openrouter",
32
+ "model": "gpt-4o-mini",
33
+ "fallbacks": [
34
+ "meta-llama/llama-3.3-70b-instruct",
35
+ "qwen/qwen2.5-72b-instruct",
36
+ ],
37
+ },
38
+ },
39
+ "latency_targets": {
40
+ "cache_hit_ms": 200,
41
+ "rule_match_ms": 50,
42
+ "ai_analysis_ms": 5000,
43
+ },
44
+ "logging": {
45
+ "level": "INFO",
46
+ "directory": str(get_log_dir()),
47
+ "max_bytes": 10485760,
48
+ "backup_count": 5,
49
+ },
50
+ "supervisor": {
51
+ "model": os.environ.get("GROQ_SUPERVISOR_MODEL", "qwen/qwen3.8-27b"),
52
+ "timeout_ms": 500,
53
+ },
54
+ "command_guide": {
55
+ "model": os.environ.get("GROQ_COMMAND_GUIDE_MODEL", "qwen/qwen3.8-27b"),
56
+ "timeout_ms": int(os.environ.get("GROQ_COMMAND_GUIDE_TIMEOUT_MS", "2000")),
57
+ },
58
+ "plugins": {
59
+ "directory": str(get_session_dir() / "plugins"),
60
+ "enabled": True,
61
+ },
62
+ "overrides_file": str(get_session_dir() / "overrides.json"),
63
+ "metrics_file": str(get_session_dir() / "metrics.json"),
64
+ "v3": {
65
+ "environment": {
66
+ "business_hours_start": 9, # 9 AM
67
+ "business_hours_end": 17, # 5 PM
68
+ "timezone": "America/Los_Angeles",
69
+ "cache_ttl_minutes": 5,
70
+ "detection_timeout_ms": 30,
71
+ },
72
+ "command_history": {
73
+ "max_commands": 1000,
74
+ "retention_days": 7,
75
+ "redact_secrets": True,
76
+ "secret_patterns": [
77
+ r"--password[= ](\S+)",
78
+ r"--token[= ](\S+)",
79
+ r"export \w*KEY\w*=",
80
+ ],
81
+ },
82
+ "query_processing": {
83
+ "timeout_ms": 100,
84
+ "cache_enabled": True,
85
+ "max_response_length": 500, # words
86
+ },
87
+ },
88
+ }
@@ -0,0 +1,94 @@
1
+ import os
2
+ import yaml
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional
5
+ from shellmate.config.paths import CONFIG_FILE, CREDENTIALS_FILE, ensure_dirs, get_cache_dir, get_log_dir, get_session_dir
6
+ from shellmate.config.defaults import get_default_config
7
+
8
+
9
+ class ConfigManager:
10
+ def __init__(self):
11
+ self._config: Optional[Dict[str, Any]] = None
12
+
13
+ def load(self) -> Dict[str, Any]:
14
+ if self._config is not None:
15
+ return self._config
16
+
17
+ ensure_dirs()
18
+ config = get_default_config()
19
+
20
+ config["cache"]["directory"] = str(get_cache_dir())
21
+ config["logging"]["directory"] = str(get_log_dir())
22
+ config["session_dir"] = str(get_session_dir())
23
+
24
+ if CONFIG_FILE.exists():
25
+ try:
26
+ with open(CONFIG_FILE, "r") as f:
27
+ user_config = yaml.safe_load(f) or {}
28
+ config = self._deep_merge(config, user_config)
29
+ except (yaml.YAMLError, OSError):
30
+ pass
31
+
32
+ self._config = config
33
+ return config
34
+
35
+ def _deep_merge(self, base: Dict, override: Dict) -> Dict:
36
+ result = base.copy()
37
+ for key, value in override.items():
38
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
39
+ result[key] = self._deep_merge(result[key], value)
40
+ else:
41
+ result[key] = value
42
+ return result
43
+
44
+ def save(self, config: Dict[str, Any]) -> None:
45
+ ensure_dirs()
46
+ with open(CONFIG_FILE, "w") as f:
47
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
48
+ self._config = config
49
+
50
+ def get(self, key: str, default=None) -> Any:
51
+ config = self.load()
52
+ keys = key.split(".")
53
+ value = config
54
+ for k in keys:
55
+ if isinstance(value, dict):
56
+ value = value.get(k)
57
+ else:
58
+ return default
59
+ if value is None:
60
+ return default
61
+ return value
62
+
63
+ def set(self, key: str, value: Any) -> None:
64
+ config = self.load()
65
+ keys = key.split(".")
66
+ target = config
67
+ for k in keys[:-1]:
68
+ if k not in target:
69
+ target[k] = {}
70
+ target = target[k]
71
+ target[keys[-1]] = value
72
+ self.save(config)
73
+
74
+
75
+ _config_manager = None
76
+
77
+
78
+ def get_config_manager() -> ConfigManager:
79
+ global _config_manager
80
+ if _config_manager is None:
81
+ _config_manager = ConfigManager()
82
+ return _config_manager
83
+
84
+
85
+ def get_config() -> Dict[str, Any]:
86
+ return get_config_manager().load()
87
+
88
+
89
+ def get_config_value(key: str, default=None) -> Any:
90
+ return get_config_manager().get(key, default)
91
+
92
+
93
+ def set_config_value(key: str, value: Any) -> None:
94
+ get_config_manager().set(key, value)