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,170 @@
1
+ import os
2
+ import json
3
+ import time
4
+ import platform
5
+ import tempfile
6
+ from pathlib import Path
7
+ from shellmate.config.paths import get_session_dir
8
+ from shellmate.cli.version import VERSION
9
+
10
+
11
+ def _session_file() -> Path:
12
+ """Resolve session.json path at call time (honors LOCALAPPDATA/XDG overrides)."""
13
+ return get_session_dir() / "session.json"
14
+
15
+
16
+ # Kept for callers that import SESSION_FILE; prefer _session_file() for correctness.
17
+ SESSION_FILE = get_session_dir() / "session.json"
18
+
19
+
20
+ def get_session_dir_path() -> Path:
21
+ return get_session_dir()
22
+
23
+
24
+ def create_session(pid: int, cwd: str, ipc_address: str) -> dict:
25
+ session_dir = get_session_dir()
26
+ session_dir.mkdir(parents=True, exist_ok=True)
27
+
28
+ if platform.system() != "Windows":
29
+ try:
30
+ session_dir.chmod(0o700)
31
+ except OSError:
32
+ pass
33
+
34
+ session = {
35
+ "pid": pid,
36
+ "cwd": cwd,
37
+ "created": time.time(),
38
+ "owner": get_owner_identity(),
39
+ "ipc_address": ipc_address,
40
+ "version": VERSION,
41
+ }
42
+
43
+ write_session_atomic(session)
44
+ return session
45
+
46
+
47
+ def get_owner_identity() -> str:
48
+ if platform.system() == "Windows":
49
+ return os.environ.get("USERNAME", "unknown")
50
+ else:
51
+ import pwd
52
+ return pwd.getpwuid(os.getuid()).pw_name
53
+
54
+
55
+ def write_session_atomic(session: dict) -> None:
56
+ session_file = _session_file()
57
+ temp_file = session_file.with_suffix(".tmp")
58
+ try:
59
+ with open(temp_file, "w") as f:
60
+ json.dump(session, f)
61
+ if platform.system() != "Windows":
62
+ os.chmod(temp_file, 0o600)
63
+ temp_file.replace(session_file)
64
+ except Exception:
65
+ if temp_file.exists():
66
+ temp_file.unlink(missing_ok=True)
67
+ raise
68
+
69
+
70
+ def read_session() -> dict:
71
+ session_file = _session_file()
72
+ if not session_file.exists():
73
+ return None
74
+ try:
75
+ with open(session_file, "r") as f:
76
+ return json.load(f)
77
+ except (json.JSONDecodeError, OSError):
78
+ return None
79
+
80
+
81
+ def remove_session() -> bool:
82
+ """Remove session directory and all v3 files."""
83
+ try:
84
+ session_dir = get_session_dir()
85
+ if session_dir.exists():
86
+ for file in session_dir.glob("*"):
87
+ file.unlink(missing_ok=True)
88
+ session_dir.rmdir()
89
+ return True
90
+ except OSError:
91
+ return False
92
+
93
+
94
+ def get_command_history_file() -> Path:
95
+ """Get path to command history JSONL file."""
96
+ session_dir = get_session_dir()
97
+ session_dir.mkdir(parents=True, exist_ok=True)
98
+ return session_dir / "command_history.jsonl"
99
+
100
+
101
+ def get_environment_cache_file() -> Path:
102
+ """Get path to environment detection cache."""
103
+ session_dir = get_session_dir()
104
+ session_dir.mkdir(parents=True, exist_ok=True)
105
+ cache_file = session_dir / "environment_cache.json"
106
+ if platform.system() != "Windows":
107
+ try:
108
+ if cache_file.exists():
109
+ cache_file.chmod(0o600)
110
+ except OSError:
111
+ pass
112
+ return cache_file
113
+
114
+
115
+ def get_query_cache_file() -> Path:
116
+ """Get path to query response cache."""
117
+ session_dir = get_session_dir()
118
+ session_dir.mkdir(parents=True, exist_ok=True)
119
+ cache_file = session_dir / "query_cache.json"
120
+ if platform.system() != "Windows":
121
+ try:
122
+ if cache_file.exists():
123
+ cache_file.chmod(0o600)
124
+ except OSError:
125
+ pass
126
+ return cache_file
127
+
128
+
129
+ def is_session_stale(session: dict) -> bool:
130
+ if not session:
131
+ return True
132
+ pid = session.get("pid")
133
+ if not pid:
134
+ return True
135
+ return not is_process_alive(pid)
136
+
137
+
138
+ def is_process_alive(pid: int) -> bool:
139
+ if platform.system() == "Windows":
140
+ try:
141
+ import subprocess
142
+ result = subprocess.run(
143
+ ["tasklist", "/FI", f"PID eq {pid}"],
144
+ capture_output=True, text=True, timeout=5
145
+ )
146
+ return str(pid) in result.stdout
147
+ except (subprocess.TimeoutExpired, FileNotFoundError):
148
+ return False
149
+ else:
150
+ try:
151
+ os.kill(pid, 0)
152
+ return True
153
+ except OSError:
154
+ return False
155
+
156
+
157
+ def cleanup_stale_sessions() -> int:
158
+ session = read_session()
159
+ if session and is_session_stale(session):
160
+ remove_session()
161
+ return 1
162
+ return 0
163
+
164
+
165
+ def validate_session_ownership(session: dict) -> bool:
166
+ if not session:
167
+ return False
168
+ current_owner = get_owner_identity()
169
+ session_owner = session.get("owner")
170
+ return current_owner == session_owner
@@ -0,0 +1,43 @@
1
+ """Startup UI — delegates to enable LiveScene (Claude Code–quiet morph)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Optional
7
+
8
+ from shellmate.ipc.client import ShellMateClient
9
+ from shellmate.ui.runtime.console_factory import ConsoleFactory
10
+ from shellmate.ui.runtime.motion_policy import motion_from_env
11
+ from shellmate.ui.scenes.enable import run_enable_scene
12
+
13
+
14
+ def run_startup_ui(timeout: int = 30, *, no_color: bool = False) -> bool:
15
+ """Poll daemon status and run the enable scene. Returns True if ready."""
16
+ client = ShellMateClient()
17
+ factory = ConsoleFactory(no_color=no_color)
18
+ console = factory.create()
19
+ plain = factory.is_plain()
20
+
21
+ def poll() -> tuple[Optional[list[str]], Optional[str]]:
22
+ try:
23
+ result = asyncio.run(client.send_command("/status", timeout=2.0))
24
+ except Exception:
25
+ return None, None
26
+ if not result or result.get("status") != "ok":
27
+ return None, None
28
+ data = result.get("data") or {}
29
+ completed = data.get("startup_stages_completed")
30
+ current = data.get("startup_stage")
31
+ if completed is not None and not isinstance(completed, list):
32
+ completed = None
33
+ return completed, current
34
+
35
+ return run_enable_scene(
36
+ poll=poll,
37
+ console=console,
38
+ plain=plain,
39
+ ascii_safe=plain,
40
+ timeout=float(timeout),
41
+ policy=motion_from_env(plain=plain),
42
+ poll_interval=0.12,
43
+ )
File without changes
@@ -0,0 +1,130 @@
1
+ import os
2
+ import platform
3
+ import re
4
+
5
+ SHELL_MAPPING = {
6
+ "bash": "bash",
7
+ "zsh": "zsh",
8
+ "fish": "fish",
9
+ "powershell": "powershell",
10
+ "pwsh": "powershell",
11
+ "cmd": "cmd",
12
+ "nu": "nushell",
13
+ }
14
+
15
+
16
+ def detect_os():
17
+ system = platform.system().lower()
18
+ if system == "linux":
19
+ return "linux"
20
+ elif system == "windows":
21
+ return "windows"
22
+ elif system == "darwin":
23
+ return "macos"
24
+ return system
25
+
26
+
27
+ def detect_shell():
28
+ shell_env = os.environ.get("SHELL", "")
29
+ if shell_env:
30
+ shell_name = os.path.basename(shell_env).lower()
31
+ for key, value in SHELL_MAPPING.items():
32
+ if key in shell_name:
33
+ return value
34
+
35
+ # Prefer parent process name (accurate under PowerShell/pwsh/bash)
36
+ try:
37
+ import psutil
38
+ parent_pid = os.getppid()
39
+ parent_proc = psutil.Process(parent_pid)
40
+ parent_name = parent_proc.name().lower()
41
+ for key, value in SHELL_MAPPING.items():
42
+ if key in parent_name:
43
+ return value
44
+ except ImportError:
45
+ pass
46
+ except Exception:
47
+ pass
48
+
49
+ if platform.system() == "Windows":
50
+ # PSModulePath is set in PowerShell sessions; COMSPEC is almost always
51
+ # cmd.exe even when the user is in pwsh — so check PS first.
52
+ ps_module_path = os.environ.get("PSModulePath", "")
53
+ if ps_module_path:
54
+ return "powershell"
55
+ comspec = os.environ.get("COMSPEC", "")
56
+ if "cmd" in comspec.lower():
57
+ return "cmd"
58
+ return "powershell"
59
+
60
+ return "unknown"
61
+
62
+
63
+ def detect_environment_type():
64
+ if platform.system() == "Windows":
65
+ wsl_distro = os.environ.get("WSL_DISTRO_NAME")
66
+ if wsl_distro:
67
+ return "wsl"
68
+ return "native"
69
+
70
+ if not platform.system() == "Linux":
71
+ return "native"
72
+
73
+ if os.environ.get("WSL_DISTRO_NAME"):
74
+ return "wsl"
75
+
76
+ try:
77
+ with open("/proc/version", "r") as f:
78
+ version = f.read().lower()
79
+ if "microsoft" in version or "wsl" in version:
80
+ return "wsl"
81
+ except (FileNotFoundError, PermissionError):
82
+ pass
83
+
84
+ container_markers = [
85
+ "/.dockerenv",
86
+ "/run/.containerenv",
87
+ ]
88
+ for marker in container_markers:
89
+ if os.path.exists(marker):
90
+ return "container"
91
+
92
+ try:
93
+ with open("/proc/1/cgroup", "r") as f:
94
+ content = f.read()
95
+ if re.search(r"docker|kubepods|garden", content, re.IGNORECASE):
96
+ return "container"
97
+ except (FileNotFoundError, PermissionError):
98
+ pass
99
+
100
+ ssh_vars = ["SSH_CLIENT", "SSH_TTY", "SSH_CONNECTION"]
101
+ for var in ssh_vars:
102
+ if os.environ.get(var):
103
+ return "ssh"
104
+
105
+ return "native"
106
+
107
+
108
+ def detect_terminal():
109
+ term_program = os.environ.get("TERM_PROGRAM", "")
110
+ if term_program:
111
+ return term_program
112
+
113
+ term = os.environ.get("TERM", "")
114
+ if term and term != "dumb":
115
+ return term
116
+
117
+ wt_session = os.environ.get("WT_SESSION", "")
118
+ if wt_session:
119
+ return "Windows Terminal"
120
+
121
+ return "unknown"
122
+
123
+
124
+ def detect_environment():
125
+ return {
126
+ "os": detect_os(),
127
+ "shell": detect_shell(),
128
+ "environment": detect_environment_type(),
129
+ "terminal": detect_terminal(),
130
+ }
File without changes
@@ -0,0 +1,210 @@
1
+ import asyncio
2
+ import json
3
+ import uuid
4
+ import threading
5
+ from shellmate.config.defaults import get_default_config
6
+
7
+
8
+ class DebouncedClient:
9
+ """Debounced API caller for autocomplete mode (300ms delay)"""
10
+
11
+ def __init__(self, delay_ms=300):
12
+ self.delay = delay_ms / 1000.0
13
+ self.timer = None
14
+ self.callback = None
15
+
16
+ def debounce(self, callback, *args, **kwargs):
17
+ """Reset timer on each call, execute only after delay"""
18
+ if self.timer:
19
+ self.timer.cancel()
20
+
21
+ self.timer = threading.Timer(
22
+ self.delay,
23
+ lambda: callback(*args, **kwargs)
24
+ )
25
+ self.timer.start()
26
+
27
+ def cancel(self):
28
+ """Cancel pending debounced call"""
29
+ if self.timer:
30
+ self.timer.cancel()
31
+
32
+ # Global instance for ble.sh plugin to use
33
+ _debounced_client = DebouncedClient(delay_ms=300)
34
+
35
+
36
+ class ShellMateClient:
37
+ def __init__(self):
38
+ self.config = get_default_config()
39
+
40
+ async def send_command(self, command, user_queries=None, overrides=None, directives=None, timeout=60.0):
41
+ cfg = self.config["daemon"]
42
+ host = cfg["host"]
43
+ port = cfg["port"]
44
+
45
+ try:
46
+ reader, writer = await asyncio.wait_for(
47
+ asyncio.open_connection(host, port),
48
+ timeout=5.0,
49
+ )
50
+
51
+ request_id = str(uuid.uuid4())
52
+ message = json.dumps({
53
+ "command": command,
54
+ "request_id": request_id,
55
+ "user_queries": user_queries or [],
56
+ "overrides": overrides or {},
57
+ "directives": directives or [],
58
+ })
59
+
60
+ writer.write(message.encode("utf-8"))
61
+ await writer.drain()
62
+
63
+ data = await asyncio.wait_for(
64
+ reader.read(1024 * 1024),
65
+ timeout=timeout,
66
+ )
67
+
68
+ writer.close()
69
+ await writer.wait_closed()
70
+ return json.loads(data.decode("utf-8"))
71
+
72
+ except asyncio.TimeoutError:
73
+ return {"status": "error", "error": "Connection timed out. Is the daemon running?"}
74
+ except ConnectionRefusedError:
75
+ return {"status": "error", "error": "Connection refused. Run 'sm enable' first."}
76
+ except Exception as e:
77
+ return {"status": "error", "error": str(e)}
78
+
79
+ async def send_context_gatherer(self, command, timeout=60.0):
80
+ cfg = self.config["daemon"]
81
+ host = cfg["host"]
82
+ port = cfg["port"]
83
+
84
+ try:
85
+ reader, writer = await asyncio.wait_for(
86
+ asyncio.open_connection(host, port),
87
+ timeout=5.0,
88
+ )
89
+
90
+ request_id = str(uuid.uuid4())
91
+ message = json.dumps({
92
+ "command": "/context-gatherer",
93
+ "request_id": request_id,
94
+ "data": {"command": command}
95
+ })
96
+
97
+ writer.write(message.encode("utf-8"))
98
+ await writer.drain()
99
+
100
+ data = await asyncio.wait_for(
101
+ reader.read(1024 * 1024),
102
+ timeout=timeout,
103
+ )
104
+
105
+ writer.close()
106
+ await writer.wait_closed()
107
+ return json.loads(data.decode("utf-8"))
108
+
109
+ except asyncio.TimeoutError:
110
+ return {"status": "error", "error": "Connection timed out. Is the daemon running?"}
111
+ except ConnectionRefusedError:
112
+ return {"status": "error", "error": "Connection refused. Run 'sm enable' first."}
113
+ except Exception as e:
114
+ return {"status": "error", "error": str(e)}
115
+
116
+ async def send_command_partial(self, partial, timeout=5.0):
117
+ """Fast partial analysis for autocomplete"""
118
+ cfg = self.config["daemon"]
119
+ host, port = cfg["host"], cfg["port"]
120
+
121
+ try:
122
+ reader, writer = await asyncio.wait_for(
123
+ asyncio.open_connection(host, port), timeout=2.0
124
+ )
125
+
126
+ message = json.dumps({
127
+ "command": "/analyze-partial",
128
+ "request_id": str(uuid.uuid4()),
129
+ "partial": partial
130
+ })
131
+
132
+ writer.write(message.encode("utf-8"))
133
+ await writer.drain()
134
+
135
+ data = await asyncio.wait_for(reader.read(1024*1024), timeout=timeout)
136
+ writer.close()
137
+ await writer.wait_closed()
138
+ return json.loads(data.decode("utf-8"))
139
+ except Exception as e:
140
+ return {"status": "error", "error": str(e)}
141
+
142
+
143
+ def analyze_command(command, user_queries=None, overrides=None, directives=None):
144
+ client = ShellMateClient()
145
+ try:
146
+ result = asyncio.run(client.send_command(command, user_queries=user_queries, overrides=overrides, directives=directives))
147
+ return result
148
+ except RuntimeError:
149
+ loop = asyncio.new_event_loop()
150
+ asyncio.set_event_loop(loop)
151
+ try:
152
+ result = loop.run_until_complete(client.send_command(command, user_queries=user_queries, overrides=overrides, directives=directives))
153
+ return result
154
+ finally:
155
+ loop.close()
156
+
157
+
158
+ def send_status():
159
+ client = ShellMateClient()
160
+ try:
161
+ result = asyncio.run(client.send_command("/status"))
162
+ return result
163
+ except RuntimeError:
164
+ loop = asyncio.new_event_loop()
165
+ asyncio.set_event_loop(loop)
166
+ try:
167
+ result = loop.run_until_complete(client.send_command("/status"))
168
+ return result
169
+ finally:
170
+ loop.close()
171
+
172
+
173
+ def send_context_gatherer(command):
174
+ client = ShellMateClient()
175
+ try:
176
+ result = asyncio.run(client.send_context_gatherer(command))
177
+ return result
178
+ except RuntimeError:
179
+ loop = asyncio.new_event_loop()
180
+ asyncio.set_event_loop(loop)
181
+ try:
182
+ result = loop.run_until_complete(client.send_context_gatherer(command))
183
+ return result
184
+ finally:
185
+ loop.close()
186
+
187
+
188
+
189
+ def analyze_partial_debounced(partial_command, callback):
190
+ """
191
+ Analyze partial command with 300ms debounce.
192
+ callback(result) is called when API responds.
193
+ """
194
+ def _do_analyze():
195
+ try:
196
+ client = ShellMateClient()
197
+ result = asyncio.run(client.send_command_partial(partial_command))
198
+ if result.get("status") == "error":
199
+ # Silent fallback: no annotation shown
200
+ callback({"annotation": "", "severity": "SAFE"})
201
+ else:
202
+ callback(result)
203
+ except (ConnectionRefusedError, asyncio.TimeoutError):
204
+ # Daemon down: silent fallback (instant tier still works)
205
+ callback({"annotation": "", "severity": "SAFE"})
206
+ except Exception as e:
207
+ # Any other error: silent fallback
208
+ callback({"annotation": "", "severity": "SAFE"})
209
+
210
+ _debounced_client.debounce(_do_analyze)