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,200 @@
1
+ import os
2
+ import re
3
+ import yaml
4
+ from shellmate.parser.command_parser import parse_command
5
+
6
+ RULES_DIR = os.path.join(os.path.dirname(__file__), "definitions")
7
+
8
+
9
+ def load_rules():
10
+ rules = []
11
+ if not os.path.isdir(RULES_DIR):
12
+ return rules
13
+
14
+ for filename in sorted(os.listdir(RULES_DIR)):
15
+ if filename.endswith((".yaml", ".yml")):
16
+ filepath = os.path.join(RULES_DIR, filename)
17
+ try:
18
+ with open(filepath, "r") as f:
19
+ file_rules = yaml.safe_load(f)
20
+ if file_rules:
21
+ for rule in file_rules:
22
+ rule["source"] = filename
23
+ rules.extend(file_rules)
24
+ except yaml.YAMLError as e:
25
+ raise ValueError(f"Error parsing {filename}: {e}")
26
+
27
+ return rules
28
+
29
+
30
+ def match_rule(command_string, rules):
31
+ parsed = parse_command(command_string)
32
+
33
+ for rule in rules:
34
+ if rule.get("pipeline") and "|" in command_string:
35
+ result = _match_pipeline_rule_on_string(command_string, rule)
36
+ else:
37
+ result = _match_single_rule(parsed, rule)
38
+ if result["matched"]:
39
+ return result
40
+
41
+ return {"matched": False, "risk": None, "confidence": 0.0}
42
+
43
+
44
+ def _match_pipeline_rule_on_string(command_string, rule):
45
+ pattern = rule.get("pattern", [])
46
+ parts = [p.strip() for p in command_string.split("|")]
47
+ if len(parts) != len([p for p in pattern if not p.get("operator")]):
48
+ return {"matched": False}
49
+
50
+ cmd_index = 0
51
+ for pattern_item in pattern:
52
+ if pattern_item.get("operator"):
53
+ continue
54
+ part = parts[cmd_index]
55
+ expected_program = pattern_item.get("program")
56
+ if expected_program and not part.startswith(expected_program):
57
+ return {"matched": False}
58
+ cmd_index += 1
59
+
60
+ return _build_match_result(rule)
61
+
62
+
63
+ def _match_single_rule(parsed_command, rule):
64
+ if isinstance(parsed_command, dict) and parsed_command.get("pipeline"):
65
+ segments = parsed_command.get("segments", [])
66
+ for seg_type, seg_data in segments:
67
+ if seg_type == "command":
68
+ result = _match_single_command_rule(seg_data, rule)
69
+ if result["matched"]:
70
+ return result
71
+ return {"matched": False}
72
+
73
+ return _match_single_command_rule(parsed_command, rule)
74
+
75
+
76
+ def _match_pipeline_rule(parsed_command, rule):
77
+ pattern = rule.get("pattern", [])
78
+
79
+ if isinstance(parsed_command, dict) and parsed_command.get("pipeline"):
80
+ segments = parsed_command.get("segments", [])
81
+ if len(segments) != len(pattern):
82
+ return {"matched": False}
83
+
84
+ for (seg_type, seg_data), pattern_item in zip(segments, pattern):
85
+ if seg_type == "command":
86
+ expected_program = pattern_item.get("program")
87
+ if isinstance(seg_data, dict):
88
+ if seg_data.get("program") != expected_program:
89
+ return {"matched": False}
90
+ elif seg_type == "pipe":
91
+ expected_op = pattern_item.get("operator")
92
+ if seg_data != expected_op:
93
+ return {"matched": False}
94
+
95
+ return _build_match_result(rule)
96
+
97
+ if isinstance(parsed_command, dict):
98
+ cmd_str = " ".join(filter(None, [
99
+ parsed_command.get("program", ""),
100
+ *([parsed_command.get("subcommand", "")] if parsed_command.get("subcommand") else []),
101
+ *([str(f) for f in parsed_command.get("flags", [])] if isinstance(parsed_command.get("flags"), list) else []),
102
+ *parsed_command.get("arguments", []),
103
+ ]))
104
+ else:
105
+ cmd_str = str(parsed_command)
106
+
107
+ if "|" not in cmd_str:
108
+ return {"matched": False}
109
+
110
+ parts = [p.strip() for p in cmd_str.split("|")]
111
+ if len(parts) != len(pattern):
112
+ return {"matched": False}
113
+
114
+ for part, pattern_item in zip(parts, pattern):
115
+ if pattern_item.get("operator"):
116
+ continue
117
+ expected_program = pattern_item.get("program")
118
+ if expected_program and not part.startswith(expected_program):
119
+ return {"matched": False}
120
+
121
+ return _build_match_result(rule)
122
+
123
+
124
+ def _match_single_command_rule(parsed_command, rule):
125
+ if isinstance(parsed_command, dict):
126
+ cmd_program = parsed_command.get("program")
127
+ else:
128
+ try:
129
+ import shlex
130
+ tokens = shlex.split(str(parsed_command))
131
+ cmd_program = tokens[0] if tokens else None
132
+ except ValueError:
133
+ return {"matched": False}
134
+
135
+ expected_program = rule.get("program")
136
+ if cmd_program != expected_program:
137
+ if expected_program and cmd_program:
138
+ if not cmd_program.startswith(expected_program + "."):
139
+ return {"matched": False}
140
+ else:
141
+ return {"matched": False}
142
+
143
+ expected_flags = rule.get("flags", [])
144
+ if expected_flags:
145
+ if isinstance(parsed_command, dict):
146
+ cmd_flags = parsed_command.get("flags", [])
147
+ flag_values = {f["flag"] for f in cmd_flags} if isinstance(cmd_flags, list) else set()
148
+ if not any(f in flag_values for f in expected_flags):
149
+ return {"matched": False}
150
+ else:
151
+ cmd_str = str(parsed_command)
152
+ if not any(f in cmd_str for f in expected_flags):
153
+ return {"matched": False}
154
+
155
+ expected_args = rule.get("arguments", [])
156
+ if expected_args:
157
+ if isinstance(parsed_command, dict):
158
+ cmd_args = parsed_command.get("arguments", [])
159
+ arg_strings = " ".join(cmd_args) if isinstance(cmd_args, list) else str(cmd_args)
160
+ else:
161
+ arg_strings = str(parsed_command)
162
+
163
+ if not any(re.search(pat, arg_strings) for pat in expected_args):
164
+ return {"matched": False}
165
+
166
+ return _build_match_result(rule)
167
+
168
+
169
+ def _build_match_result(rule):
170
+ return {
171
+ "matched": True,
172
+ "rule_id": rule.get("id"),
173
+ "category": rule.get("category"),
174
+ "severity": rule.get("severity", "MEDIUM"),
175
+ "risk": rule.get("severity", "MEDIUM"),
176
+ "confidence": rule.get("confidence", 0.8),
177
+ "description": rule.get("message", rule.get("description", "")),
178
+ "message": rule.get("message", rule.get("description", "")),
179
+ "rule": rule,
180
+ }
181
+
182
+
183
+ def validate_rules():
184
+ rules = load_rules()
185
+ rule_ids = {}
186
+ for rule in rules:
187
+ rid = rule.get("id")
188
+ if not rid:
189
+ raise ValueError(f"Rule missing 'id' in {rule.get('source', 'unknown')}")
190
+ if rid in rule_ids:
191
+ raise ValueError(f"Duplicate rule ID: {rid}")
192
+ rule_ids[rid] = True
193
+ if not rule.get("category"):
194
+ raise ValueError(f"Rule {rid} missing 'category'")
195
+ if not rule.get("severity"):
196
+ raise ValueError(f"Rule {rid} missing 'severity'")
197
+ if not rule.get("program") and not rule.get("pipeline"):
198
+ raise ValueError(f"Rule {rid} missing 'program' or 'pipeline'")
199
+
200
+ return rules
File without changes
@@ -0,0 +1,114 @@
1
+ import re
2
+ import shlex
3
+
4
+ TRAILING_CHARS = re.compile(r"[,;.]$")
5
+ MULTI_SPACE = re.compile(r"\s{2,}")
6
+
7
+
8
+ def normalize_whitespace(command):
9
+ return MULTI_SPACE.sub(" ", command.strip())
10
+
11
+
12
+ def detect_trailing_char(command):
13
+ stripped = command.strip()
14
+ match = TRAILING_CHARS.search(stripped)
15
+ if match:
16
+ trailing = match.group()
17
+ corrected = stripped.rstrip(",. ;")
18
+ return {
19
+ "detected": True,
20
+ "trailing": trailing,
21
+ "original": command,
22
+ "suggestion": corrected,
23
+ "message": f"Did you mean: {corrected}",
24
+ }
25
+ return {"detected": False}
26
+
27
+
28
+ def detect_duplicate_flags(tokens):
29
+ seen = []
30
+ duplicates = []
31
+ for token in tokens:
32
+ if token.startswith("-"):
33
+ if token in seen:
34
+ duplicates.append(token)
35
+ else:
36
+ seen.append(token)
37
+ if duplicates:
38
+ return {
39
+ "detected": True,
40
+ "duplicates": duplicates,
41
+ "message": f"Duplicate flag(s) detected: {', '.join(duplicates)}",
42
+ }
43
+ return {"detected": False}
44
+
45
+
46
+ def validate_syntax(command):
47
+ in_single = False
48
+ in_double = False
49
+ i = 0
50
+ char_count = len(command)
51
+
52
+ errors = []
53
+
54
+ while i < char_count:
55
+ c = command[i]
56
+ if c == "\\" and i + 1 < char_count:
57
+ i += 2
58
+ continue
59
+ if c == "'" and not in_double:
60
+ in_single = not in_single
61
+ elif c == '"' and not in_single:
62
+ in_double = not in_double
63
+ i += 1
64
+
65
+ if in_single:
66
+ errors.append("Unmatched single quote")
67
+ if in_double:
68
+ errors.append("Unmatched double quote")
69
+
70
+ if not errors:
71
+ try:
72
+ shlex.split(command)
73
+ except ValueError as e:
74
+ errors.append(str(e))
75
+
76
+ token_pattern = re.findall(r'(?:^|\s)([<>]{2,3})(?:\s|$)', command)
77
+ for token in token_pattern:
78
+ if token not in (">", "<", ">>", "<<", ">&", "<>", "2>", "2>>"):
79
+ if token not in errors:
80
+ errors.append(f"Invalid redirect operator: {token}")
81
+
82
+ return {
83
+ "valid": len(errors) == 0,
84
+ "errors": errors,
85
+ }
86
+
87
+
88
+ def sanitize(command):
89
+ result = {
90
+ "original": command,
91
+ "normalized": normalize_whitespace(command),
92
+ "trailing_char": None,
93
+ "duplicate_flags": None,
94
+ "syntax": None,
95
+ "correction": None,
96
+ }
97
+
98
+ trailing = detect_trailing_char(result["normalized"])
99
+ if trailing["detected"]:
100
+ result["trailing_char"] = trailing
101
+ result["correction"] = trailing["suggestion"]
102
+
103
+ try:
104
+ tokens = shlex.split(result["normalized"])
105
+ dup = detect_duplicate_flags(tokens)
106
+ if dup["detected"]:
107
+ result["duplicate_flags"] = dup
108
+ except ValueError:
109
+ tokens = result["normalized"].split()
110
+
111
+ syntax = validate_syntax(result["normalized"])
112
+ result["syntax"] = syntax
113
+
114
+ return result
@@ -0,0 +1,10 @@
1
+ from shellmate.ui.analysis import render_analysis
2
+ from shellmate.ui.status import render_status
3
+ from shellmate.ui.banner import print_banner, print_usage
4
+
5
+ __all__ = [
6
+ "render_analysis",
7
+ "render_status",
8
+ "print_banner",
9
+ "print_usage",
10
+ ]
@@ -0,0 +1,164 @@
1
+ from rich.console import Console, Group
2
+ from rich.table import Table
3
+ from rich.text import Text
4
+ from rich.panel import Panel
5
+ from rich.columns import Columns
6
+ from rich.rule import Rule
7
+
8
+ console = Console(width=120)
9
+
10
+ RISK_STYLES = {
11
+ "SAFE": ("green", "[OK]", "bold green", "green"),
12
+ "LOW": ("blue", "[i]", "bold blue", "blue"),
13
+ "MEDIUM": ("yellow", "[!]", "bold yellow", "yellow"),
14
+ "HIGH": ("red", "[!!]", "bold red", "red"),
15
+ "CRITICAL": ("bold red", "[!!!]", "bold white on red", "red"),
16
+ }
17
+
18
+ SEVERITY_ORDER = {"SAFE": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4}
19
+
20
+
21
+ def _confidence_bar(confidence: float) -> str:
22
+ bar_len = 12
23
+ filled = int(confidence * bar_len)
24
+ bar = "█" * filled + "░" * (bar_len - filled)
25
+ color = "green" if confidence >= 0.8 else "yellow" if confidence >= 0.5 else "red"
26
+ return f"[{color}]{bar}[/{color}]"
27
+
28
+
29
+ def _latency_table(latency: dict) -> Table:
30
+ table = Table(show_header=False, box=None, padding=(0, 2))
31
+ table.add_column("Stage", style="dim")
32
+ table.add_column("Time", justify="right")
33
+ for stage, ms in sorted(latency.items(), key=lambda x: -x[1]):
34
+ ms_str = f"{ms:.0f}ms" if ms > 0 else "<1ms"
35
+ table.add_row(f" {stage}", ms_str)
36
+ return table
37
+
38
+
39
+ def render_analysis(result: dict):
40
+ risk = result.get("risk", "UNKNOWN").upper()
41
+ confidence = result.get("confidence", 0.0)
42
+ message = result.get("message", "") or result.get("explanation", "")
43
+ alternatives = result.get("alternatives", [])
44
+ context_used = result.get("context_used", {})
45
+ latency = result.get("latency", {})
46
+ total_ms = result.get("total_ms", 0)
47
+ early_exit = result.get("early_exit")
48
+ cached = result.get("cached", False)
49
+ warning = result.get("supervisor_warning")
50
+ guide = result.get("command_guide")
51
+
52
+ color, prefix, risk_style, border = RISK_STYLES.get(risk, ("white", "[?]", "white", "white"))
53
+
54
+ risk_header = Text()
55
+ risk_header.append(f"{prefix} {risk}", style=risk_style)
56
+ if cached:
57
+ risk_header.append(" (cached)", style="dim italic")
58
+
59
+ elements = [risk_header]
60
+
61
+ if confidence > 0:
62
+ bar = _confidence_bar(confidence)
63
+ elements.append(Text.assemble(" ", bar, f" {confidence:.0%}", style="dim"))
64
+
65
+ if message:
66
+ max_len = 500
67
+ truncated = message[:max_len] + "..." if len(message) > max_len else message
68
+ msg = Text(truncated)
69
+ msg.stylize("white")
70
+ if len(truncated) > 100:
71
+ elements.append(Panel(msg, border_style=border, padding=(0, 1)))
72
+ else:
73
+ elements.append(Text(f" {truncated}"))
74
+
75
+ failed_cred = result.get("failed_credential")
76
+ failed_prov = result.get("failed_provider")
77
+ if failed_cred or failed_prov:
78
+ which = failed_cred or (f"{failed_prov}_API_KEY".upper() if failed_prov else "API key")
79
+ elements.append(
80
+ Text(f" Failed credential: {which}", style="bold yellow")
81
+ )
82
+
83
+ if early_exit:
84
+ exit_msgs = {
85
+ "sanitizer": "Corrected by typo sanitizer",
86
+ "rule_engine": "Matched deterministic rule",
87
+ "cache": "Returned from cache",
88
+ }
89
+ exit_label = exit_msgs.get(early_exit, early_exit)
90
+ elements.append(Text(f" [dim]Early exit:[/] [italic]{exit_label}[/]"))
91
+
92
+ if warning:
93
+ warning_panel = Panel(
94
+ Text(f"! {warning}", style="yellow"),
95
+ border_style="yellow",
96
+ padding=(0, 1),
97
+ )
98
+ elements.append(warning_panel)
99
+
100
+ if alternatives:
101
+ alt_label = "Suggestions:" if risk in ("SAFE", "LOW") else "Safer alternatives:"
102
+ alt_parts = [Text(alt_label, style="bold")]
103
+ for i, alt in enumerate(alternatives[:5], 1):
104
+ alt_parts.append(Text(f" {i}. {alt}"))
105
+ elements.append(Group(*alt_parts))
106
+
107
+ if context_used:
108
+ ctx_parts = []
109
+ if context_used.get("watcher"):
110
+ ctx_parts.append("watcher")
111
+ if context_used.get("librarian"):
112
+ ctx_parts.append("librarian")
113
+ if ctx_parts:
114
+ ct = Text(f" Context: {', '.join(ctx_parts)}")
115
+ ct.stylize("dim italic")
116
+ elements.append(ct)
117
+
118
+ if guide:
119
+ guide_parts = []
120
+ if guide.get("intent_guess"):
121
+ guide_parts.append(Text(guide["intent_guess"], style="italic"))
122
+ if guide.get("when_to_use"):
123
+ guide_parts.append(Text(f" When to use: {guide['when_to_use']}"))
124
+ if guide.get("parameters"):
125
+ guide_parts.append(Text(" Parameters:", style="bold dim"))
126
+ for p in guide.get("parameters") or []:
127
+ guide_parts.append(Text(f" • {p}"))
128
+ suggested = guide.get("suggested_command") or guide.get("example_command")
129
+ if suggested:
130
+ guide_parts.append(Text.assemble(
131
+ " Use: ", (suggested, "bold cyan"),
132
+ ))
133
+ if guide_parts:
134
+ guide_panel = Panel(
135
+ Group(*guide_parts),
136
+ border_style="cyan",
137
+ padding=(0, 1),
138
+ title="[bold cyan]Command guide[/bold cyan]",
139
+ )
140
+ elements.append(guide_panel)
141
+
142
+ if latency:
143
+ elements.append(Text("Latency breakdown:", style="bold dim"))
144
+ elements.append(_latency_table(latency))
145
+
146
+ if total_ms:
147
+ elements.append(Text(f" Total: {total_ms:.0f}ms", style="dim"))
148
+
149
+ main_panel = Panel(
150
+ Group(*elements),
151
+ border_style=border,
152
+ padding=(1, 2),
153
+ title=f"[{color}]{risk} Analysis[/{color}]",
154
+ title_align="left",
155
+ )
156
+ console.print(main_panel)
157
+
158
+ suggested_cmd = guide and (guide.get("suggested_command") or guide.get("example_command"))
159
+ if suggested_cmd:
160
+ tip = Text.assemble(
161
+ ("> Tip: ", "bold yellow"),
162
+ (f"Try `{suggested_cmd}` for this operation", "white"),
163
+ )
164
+ console.print(tip)