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.
- shellmate/__init__.py +0 -0
- shellmate/adapters/__init__.py +67 -0
- shellmate/adapters/bash_adapter.sh +7 -0
- shellmate/adapters/ble_adapter.sh +350 -0
- shellmate/adapters/powershell_adapter.ps1 +417 -0
- shellmate/adapters/powershell_predictor.ps1 +97 -0
- shellmate/adapters/zsh_adapter.zsh +8 -0
- shellmate/agents/__init__.py +23 -0
- shellmate/agents/base.py +248 -0
- shellmate/agents/chain_patterns.yaml +98 -0
- shellmate/agents/command_chain.py +322 -0
- shellmate/agents/context_gatherer.py +175 -0
- shellmate/agents/environment.py +363 -0
- shellmate/agents/librarian.py +502 -0
- shellmate/agents/query.py +415 -0
- shellmate/agents/repomix_backend.py +171 -0
- shellmate/agents/watcher.py +266 -0
- shellmate/cache/__init__.py +0 -0
- shellmate/cache/semantic_cache.py +41 -0
- shellmate/cli/__init__.py +0 -0
- shellmate/cli/commands/__init__.py +27 -0
- shellmate/cli/commands/analyze.py +5 -0
- shellmate/cli/commands/config.py +319 -0
- shellmate/cli/commands/configure.py +118 -0
- shellmate/cli/commands/disable.py +11 -0
- shellmate/cli/commands/enable.py +49 -0
- shellmate/cli/commands/init.py +412 -0
- shellmate/cli/commands/install.py +242 -0
- shellmate/cli/commands/migrate.py +147 -0
- shellmate/cli/commands/status.py +31 -0
- shellmate/cli/commands/uninstall.py +78 -0
- shellmate/cli/main.py +114 -0
- shellmate/cli/parser.py +184 -0
- shellmate/cli/version.py +1 -0
- shellmate/completion/__init__.py +4 -0
- shellmate/completion/bash_adapter.py +145 -0
- shellmate/config/__init__.py +33 -0
- shellmate/config/credentials.py +120 -0
- shellmate/config/defaults.py +88 -0
- shellmate/config/manager.py +94 -0
- shellmate/config/paths.py +77 -0
- shellmate/config/secrets.py +66 -0
- shellmate/daemon/__init__.py +0 -0
- shellmate/daemon/lifecycle.py +308 -0
- shellmate/daemon/observability.py +82 -0
- shellmate/daemon/pipeline.py +119 -0
- shellmate/daemon/server.py +605 -0
- shellmate/daemon/session.py +170 -0
- shellmate/daemon/startup_ui.py +43 -0
- shellmate/environment/__init__.py +0 -0
- shellmate/environment/detector.py +130 -0
- shellmate/ipc/__init__.py +0 -0
- shellmate/ipc/client.py +210 -0
- shellmate/ipc/ps_client.py +190 -0
- shellmate/orchestration/__init__.py +3 -0
- shellmate/orchestration/analysis_graph.py +1274 -0
- shellmate/orchestration/state.py +35 -0
- shellmate/parser/__init__.py +0 -0
- shellmate/parser/command_parser.py +111 -0
- shellmate/plugins/__init__.py +0 -0
- shellmate/plugins/base.py +80 -0
- shellmate/plugins/loader.py +41 -0
- shellmate/providers/__init__.py +0 -0
- shellmate/providers/base.py +97 -0
- shellmate/providers/groq_command_guide.py +108 -0
- shellmate/providers/groq_provider.py +127 -0
- shellmate/providers/groq_supervisor.py +78 -0
- shellmate/providers/openrouter_provider.py +149 -0
- shellmate/providers/router.py +272 -0
- shellmate/providers/validate_keys.py +98 -0
- shellmate/providers/xai_provider.py +142 -0
- shellmate/rules/__init__.py +0 -0
- shellmate/rules/catastrophic.yaml +96 -0
- shellmate/rules/catastrophic_matcher.py +167 -0
- shellmate/rules/definitions/__init__.py +0 -0
- shellmate/rules/definitions/file_deletion.yaml +54 -0
- shellmate/rules/definitions/filesystem.yaml +43 -0
- shellmate/rules/definitions/network_execution.yaml +46 -0
- shellmate/rules/definitions/permissions.yaml +54 -0
- shellmate/rules/definitions/process_control.yaml +37 -0
- shellmate/rules/definitions/system_destruction.yaml +67 -0
- shellmate/rules/engine.py +200 -0
- shellmate/sanitizer/__init__.py +0 -0
- shellmate/sanitizer/typo_detector.py +114 -0
- shellmate/ui/__init__.py +10 -0
- shellmate/ui/analysis.py +164 -0
- shellmate/ui/annotations.py +232 -0
- shellmate/ui/banner.py +49 -0
- shellmate/ui/progress.py +126 -0
- shellmate/ui/runtime/__init__.py +15 -0
- shellmate/ui/runtime/console_factory.py +65 -0
- shellmate/ui/runtime/live_scene.py +116 -0
- shellmate/ui/runtime/motion_policy.py +69 -0
- shellmate/ui/runtime/tokens.py +47 -0
- shellmate/ui/scenes/__init__.py +24 -0
- shellmate/ui/scenes/enable.py +316 -0
- shellmate/ui/scenes/help.py +53 -0
- shellmate/ui/scenes/stages.py +128 -0
- shellmate/ui/scenes/status.py +181 -0
- shellmate/ui/status.py +3 -0
- shellmate_cli-3.0.0.dist-info/METADATA +352 -0
- shellmate_cli-3.0.0.dist-info/RECORD +106 -0
- shellmate_cli-3.0.0.dist-info/WHEEL +5 -0
- shellmate_cli-3.0.0.dist-info/entry_points.txt +3 -0
- shellmate_cli-3.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
shellmate/ui/__init__.py
ADDED
shellmate/ui/analysis.py
ADDED
|
@@ -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)
|