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
shellmate/__init__.py ADDED
File without changes
@@ -0,0 +1,67 @@
1
+ import shutil
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ ADAPTERS_DIR = Path(__file__).parent
8
+
9
+
10
+ def get_adapter_path(shell: str) -> Optional[Path]:
11
+ mapping = {
12
+ "bash": ADAPTERS_DIR / "bash_adapter.sh",
13
+ "zsh": ADAPTERS_DIR / "zsh_adapter.zsh",
14
+ "powershell": ADAPTERS_DIR / "powershell_adapter.ps1",
15
+ }
16
+ path = mapping.get(shell)
17
+ return path if path and path.exists() else None
18
+
19
+
20
+ def get_supported_shells() -> list[str]:
21
+ return ["bash", "zsh", "powershell"]
22
+
23
+
24
+ def check_shell_available(shell: str) -> bool:
25
+ if shell == "powershell":
26
+ return sys.platform == "win32"
27
+ return shutil.which(shell) is not None
28
+
29
+
30
+ def validate_adapter_syntax(shell: str) -> tuple[bool, str]:
31
+ path = get_adapter_path(shell)
32
+ if not path:
33
+ return False, f"Adapter not found for {shell}"
34
+
35
+ content = path.read_text(encoding="utf-8")
36
+
37
+ if shell == "powershell":
38
+ try:
39
+ result = subprocess.run(
40
+ ["powershell", "-NoProfile", "-Command",
41
+ f"$errors = $null; [System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw '{path}'), [ref]$null, [ref]$errors); if ($errors) {{ $errors | ForEach-Object {{ $_.Message }} }} else {{ 'OK' }}"],
42
+ capture_output=True, text=True, timeout=15
43
+ )
44
+ output = result.stdout.strip()
45
+ if output == "OK":
46
+ return True, "Syntax OK"
47
+ return False, f"Syntax errors: {output}"
48
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
49
+ return False, f"Cannot validate: {e}"
50
+
51
+ if shell in ("bash", "zsh"):
52
+ interpreter = shutil.which(shell)
53
+ if not interpreter:
54
+ return False, f"{shell} not available on this system"
55
+
56
+ try:
57
+ result = subprocess.run(
58
+ [interpreter, "-n", str(path)],
59
+ capture_output=True, text=True, timeout=15
60
+ )
61
+ if result.returncode == 0:
62
+ return True, "Syntax OK"
63
+ return False, f"Syntax errors: {result.stderr.strip()}"
64
+ except (subprocess.TimeoutExpired, OSError) as e:
65
+ return False, f"Cannot validate: {e}"
66
+
67
+ return False, f"Unsupported shell: {shell}"
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ # ShellMate Bash Adapter - Autocomplete Mode Only
3
+ # This file is deprecated - use ble_adapter.sh for autocomplete mode
4
+ # Kept for backward compatibility only
5
+
6
+ echo "⚠️ ShellMate: bash_adapter.sh is deprecated. Use ble_adapter.sh for autocomplete mode."
7
+ echo " Run: sm migrate-to-autocomplete"
@@ -0,0 +1,350 @@
1
+ #!/bin/bash
2
+ # ShellMate ble.sh adapter — two-phase autocomplete annotations
3
+ #
4
+ # Phase 1 (instant): catastrophic matcher → placeholder annotation
5
+ # Phase 2 (debounced 300ms): /analyze-partial → replace annotation in-place
6
+ # Tab×2: accept completion → consequence preview below prompt
7
+
8
+ # Global state
9
+ _SHELLMATE_NEEDS_CONFIRM=""
10
+ _SHELLMATE_LAST_DANGEROUS=""
11
+ _SHELLMATE_LAST_ANNOTATION=""
12
+ _SHELLMATE_DEBOUNCE_PID=""
13
+ _SHELLMATE_ROOT=""
14
+ _SHELLMATE_TAB_STATE="" # "" | "suggested" | "consequence_shown"
15
+ _SHELLMATE_TAB_CMD="" # full command string after completion accepted
16
+ _SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM="" # cmd waiting for second Enter
17
+ _SHELLMATE_CONSEQUENCE_CONFIRM_ARMED="" # set after first Enter blocked
18
+
19
+ _shellmate_resolve_root() {
20
+ if [[ -n "$_SHELLMATE_ROOT" ]]; then
21
+ echo "$_SHELLMATE_ROOT"
22
+ return
23
+ fi
24
+ local root
25
+ root=$(python3 -c "import shellmate, os; print(os.path.dirname(shellmate.__file__))" 2>/dev/null)
26
+ if [[ -z "$root" ]]; then
27
+ # Fallback: adapter lives in <repo>/adapters/
28
+ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
29
+ fi
30
+ _SHELLMATE_ROOT="$root"
31
+ echo "$root"
32
+ }
33
+
34
+ _shellmate_render_annotation() {
35
+ # Render annotation below command line without moving cursor permanently.
36
+ # Uses ble.sh canvas when available; falls back to ANSI save/restore.
37
+ local text="$1"
38
+ _SHELLMATE_LAST_ANNOTATION="$text"
39
+ if type ble/canvas/put.draw &>/dev/null; then
40
+ ble/canvas/put.draw "\e[91m${text}\e[m"
41
+ else
42
+ # ANSI: save cursor, move down, clear line, write, restore
43
+ printf '\e[s\e[1B\e[2K%s\e[u' "$text" >/dev/tty 2>/dev/null || true
44
+ fi
45
+ }
46
+
47
+ _shellmate_clear_annotation() {
48
+ _SHELLMATE_LAST_ANNOTATION=""
49
+ if type ble/canvas/put.draw &>/dev/null; then
50
+ ble/canvas/put.draw ""
51
+ else
52
+ printf '\e[s\e[1B\e[2K\e[u' >/dev/tty 2>/dev/null || true
53
+ fi
54
+ }
55
+
56
+ _shellmate_render_consequence() {
57
+ # Render up to 3 consequence lines below the prompt using ANSI save/restore.
58
+ local line1="$1" line2="$2" line3="${3:-}"
59
+ if type ble/canvas/put.draw &>/dev/null; then
60
+ ble/canvas/put.draw "${line1}"
61
+ else
62
+ printf '\e[s\e[1B\e[2K%b' "${line1}" >/dev/tty 2>/dev/null || true
63
+ printf '\e[2B\e[2K%b' "${line2}" >/dev/tty 2>/dev/null || true
64
+ if [[ -n "$line3" ]]; then
65
+ printf '\e[3B\e[2K%b' "${line3}" >/dev/tty 2>/dev/null || true
66
+ fi
67
+ printf '\e[u' >/dev/tty 2>/dev/null || true
68
+ fi
69
+ }
70
+
71
+ _shellmate_show_consequence_async() {
72
+ local cmd="$1"
73
+ # Show placeholder immediately
74
+ _shellmate_render_consequence "\e[90m ⚙ Analyzing…\e[m" "" ""
75
+ _shellmate_cancel_debounce
76
+
77
+ (
78
+ local sm_path
79
+ sm_path=$(_shellmate_resolve_root)
80
+ local raw
81
+ raw=$(timeout 1.5 python3 -c "
82
+ import sys, json, threading
83
+ sys.path.insert(0, r'''$sm_path''')
84
+ from ipc.client import analyze_partial_debounced
85
+ holder = {}
86
+ done = threading.Event()
87
+ def _cb(r): holder['r'] = r; done.set()
88
+ analyze_partial_debounced(sys.argv[1], _cb)
89
+ done.wait(timeout=1.2)
90
+ result = holder.get('r') or {}
91
+ cp = result.get('consequence_preview') or {}
92
+ print(json.dumps({
93
+ 'what_happens': cp.get('what_happens', ''),
94
+ 'scope': cp.get('scope', ''),
95
+ 'reversible': cp.get('reversible', True),
96
+ 'suggested_fix': cp.get('suggested_fix', ''),
97
+ 'severity': result.get('severity', 'MEDIUM'),
98
+ }))
99
+ " "$cmd" 2>/dev/null)
100
+
101
+ if [[ -z "$raw" ]]; then return; fi
102
+
103
+ local what_happens scope reversible suggested_fix severity
104
+ what_happens=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('what_happens',''))" "$raw" 2>/dev/null)
105
+ scope=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('scope',''))" "$raw" 2>/dev/null)
106
+ reversible=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print('No' if not d.get('reversible',True) else 'Yes')" "$raw" 2>/dev/null)
107
+ suggested_fix=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('suggested_fix',''))" "$raw" 2>/dev/null)
108
+ severity=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('severity','MEDIUM'))" "$raw" 2>/dev/null)
109
+
110
+ local color="\e[93m"
111
+ [[ "$severity" == "HIGH" || "$severity" == "CRITICAL" || "$severity" == "blocking" ]] && color="\e[91m"
112
+ [[ "$severity" == "SAFE" || "$severity" == "LOW" ]] && color="\e[92m"
113
+
114
+ local line1 line2 line3=""
115
+ line1="${color} → ${what_happens}\e[m"
116
+ line2="${color} → Reversible: ${reversible} | ${scope}\e[m"
117
+ [[ -n "$suggested_fix" ]] && line3="\e[90m → Safe alt: ${suggested_fix}\e[m"
118
+
119
+ # Gate HIGH/CRITICAL on second Enter
120
+ if [[ "$severity" == "HIGH" || "$severity" == "CRITICAL" || "$severity" == "blocking" ]]; then
121
+ _SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM="$cmd"
122
+ line2="${line2} \e[91m[Press Enter again to execute]\e[m"
123
+ fi
124
+
125
+ _shellmate_render_consequence "$line1" "$line2" "$line3"
126
+ ) &
127
+ _SHELLMATE_DEBOUNCE_PID=$!
128
+ }
129
+
130
+ sm_instant_check() {
131
+ # Returns 0 if catastrophic match, 1 otherwise. Prints JSON to stdout.
132
+ local cmd="$1"
133
+ local sm_path
134
+ sm_path=$(_shellmate_resolve_root)
135
+ [[ -z "$sm_path" ]] && return 1
136
+
137
+ local result
138
+ result=$(python3 -c "
139
+ import sys, json
140
+ sys.path.insert(0, r'''$sm_path''')
141
+ from rules.catastrophic_matcher import match_catastrophic
142
+ cmd = sys.argv[1]
143
+ print(json.dumps(match_catastrophic(cmd)))
144
+ " "$cmd" 2>/dev/null)
145
+
146
+ if [[ "$result" == *'"matched": true'* ]] || [[ "$result" == *'"matched":true'* ]]; then
147
+ echo "$result"
148
+ return 0
149
+ fi
150
+ echo "$result"
151
+ return 1
152
+ }
153
+
154
+ _shellmate_cancel_debounce() {
155
+ if [[ -n "$_SHELLMATE_DEBOUNCE_PID" ]]; then
156
+ kill "$_SHELLMATE_DEBOUNCE_PID" 2>/dev/null || true
157
+ _SHELLMATE_DEBOUNCE_PID=""
158
+ fi
159
+ python3 -c "
160
+ import sys
161
+ try:
162
+ sys.path.insert(0, r'''$(_shellmate_resolve_root)''')
163
+ from ipc.client import _debounced_client
164
+ _debounced_client.cancel()
165
+ except Exception:
166
+ pass
167
+ " 2>/dev/null || true
168
+ }
169
+
170
+ _shellmate_start_debounce() {
171
+ # Pivot Phase 3 Task 3.3: use ipc.client.analyze_partial_debounced (300ms Timer)
172
+ local cmd="$1"
173
+ _shellmate_cancel_debounce
174
+
175
+ (
176
+ local sm_path
177
+ sm_path=$(_shellmate_resolve_root)
178
+ local annotation
179
+ annotation=$(timeout 1.2 python3 -c "
180
+ import sys
181
+ import threading
182
+ sys.path.insert(0, r'''$sm_path''')
183
+ from ipc.client import analyze_partial_debounced
184
+
185
+ holder = {}
186
+ done = threading.Event()
187
+
188
+ def _cb(result):
189
+ holder['r'] = result
190
+ done.set()
191
+
192
+ analyze_partial_debounced(sys.argv[1], _cb)
193
+ if not done.wait(timeout=1.0):
194
+ print('')
195
+ raise SystemExit(0)
196
+
197
+ result = holder.get('r') or {}
198
+ ann = result.get('annotation') or result.get('consequence_preview') or ''
199
+ if isinstance(ann, dict):
200
+ ann = ann.get('what_happens') or ann.get('summary') or ''
201
+ sev = str(result.get('severity', 'MEDIUM'))
202
+ if not ann:
203
+ print('')
204
+ else:
205
+ if sev in ('blocking', 'CRITICAL', 'HIGH'):
206
+ icon = '⚠️'
207
+ elif sev in ('SAFE', 'info', 'LOW'):
208
+ icon = 'ℹ️'
209
+ else:
210
+ icon = 'ℹ️'
211
+ print(f'{icon} {str(ann)[:80]}')
212
+ " "$cmd" 2>/dev/null)
213
+ if [[ -n "$annotation" ]]; then
214
+ # Replace placeholder in-place (do not append)
215
+ _shellmate_render_annotation "$annotation"
216
+ fi
217
+ ) &
218
+ _SHELLMATE_DEBOUNCE_PID=$!
219
+ }
220
+
221
+ # --- ble.sh widget: keystroke / pre-exec path ---
222
+
223
+ function ble/widget/shellmate-on-keystroke {
224
+ local cmd="${READLINE_LINE}"
225
+
226
+ # Skip empty / very short input
227
+ if [[ ${#cmd} -lt 2 ]]; then
228
+ _shellmate_clear_annotation
229
+ return 0
230
+ fi
231
+
232
+ # Phase 1: Instant catastrophic check (target <5ms with cached rules)
233
+ local instant_json
234
+ if instant_json=$(sm_instant_check "$cmd"); then
235
+ _SHELLMATE_NEEDS_CONFIRM="$cmd"
236
+ _shellmate_cancel_debounce
237
+ _shellmate_render_annotation "⚡ BLOCKING — Analyzing..."
238
+ # Kick pipeline anyway so placeholder is replaced with full consequence
239
+ _shellmate_start_debounce "$cmd"
240
+ else
241
+ _SHELLMATE_NEEDS_CONFIRM=""
242
+ # Phase 2: debounce full pipeline for non-catastrophic
243
+ _shellmate_start_debounce "$cmd"
244
+ fi
245
+ }
246
+
247
+ function ble/widget/shellmate-check-catastrophic {
248
+ # PRECMD_EXEC / PREEXEC path — keep confirm double-tap semantics
249
+ local line="${READLINE_LINE}"
250
+ local instant_json
251
+ if instant_json=$(sm_instant_check "$line"); then
252
+ _SHELLMATE_NEEDS_CONFIRM="$line"
253
+ _shellmate_render_annotation "⚡ BLOCKING — Type again to confirm"
254
+ else
255
+ _SHELLMATE_NEEDS_CONFIRM=""
256
+ fi
257
+ }
258
+
259
+ function ble/widget/shellmate-before-exec {
260
+ # Gate 1: catastrophic double-tap
261
+ if [[ -n "$_SHELLMATE_NEEDS_CONFIRM" ]]; then
262
+ if [[ "$_SHELLMATE_LAST_DANGEROUS" != "$_SHELLMATE_NEEDS_CONFIRM" ]]; then
263
+ _SHELLMATE_LAST_DANGEROUS="$_SHELLMATE_NEEDS_CONFIRM"
264
+ echo -e "\e[91m⛔ Confirmation required. Type command again.\e[m"
265
+ return 1
266
+ fi
267
+ fi
268
+
269
+ # Gate 2: consequence preview requires second Enter for HIGH/CRITICAL
270
+ local current_cmd="${READLINE_LINE}"
271
+ if [[ -n "$_SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM" && \
272
+ "$_SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM" == "$current_cmd" ]]; then
273
+ if [[ "$_SHELLMATE_CONSEQUENCE_CONFIRM_ARMED" != "$current_cmd" ]]; then
274
+ _SHELLMATE_CONSEQUENCE_CONFIRM_ARMED="$current_cmd"
275
+ echo -e "\e[91m[Press Enter again to execute]\e[m"
276
+ return 1
277
+ fi
278
+ _SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM=""
279
+ _SHELLMATE_CONSEQUENCE_CONFIRM_ARMED=""
280
+ else
281
+ _SHELLMATE_CONSEQUENCE_CONFIRM_ARMED=""
282
+ fi
283
+
284
+ _SHELLMATE_LAST_DANGEROUS=""
285
+ _SHELLMATE_TAB_STATE=""
286
+ _SHELLMATE_TAB_CMD=""
287
+ return 0
288
+ }
289
+
290
+ function ble/widget/shellmate-cancel-debounce {
291
+ _shellmate_cancel_debounce
292
+ }
293
+
294
+ function ble/widget/shellmate-complete {
295
+ local line="${READLINE_LINE}"
296
+
297
+ # Second Tab: suggestion already shown → accept it and trigger consequence
298
+ if [[ "$_SHELLMATE_TAB_STATE" == "suggested" && -n "$_SHELLMATE_TAB_CMD" ]]; then
299
+ READLINE_LINE="$_SHELLMATE_TAB_CMD"
300
+ READLINE_POINT=${#READLINE_LINE}
301
+ _SHELLMATE_TAB_STATE="consequence_shown"
302
+ _shellmate_clear_annotation
303
+ _shellmate_show_consequence_async "$_SHELLMATE_TAB_CMD"
304
+ return
305
+ fi
306
+
307
+ # Reset state on fresh Tab
308
+ _SHELLMATE_TAB_STATE=""
309
+ _SHELLMATE_TAB_CMD=""
310
+ [[ ${#line} -lt 2 ]] && return
311
+
312
+ local sm_path
313
+ sm_path=$(_shellmate_resolve_root)
314
+ [[ -z "$sm_path" ]] && return
315
+
316
+ local completions
317
+ completions=$(python3 -c "
318
+ import sys
319
+ sys.path.insert(0, r'''$sm_path''')
320
+ from completion.bash_adapter import get_completions
321
+ print('\n'.join(get_completions(sys.argv[1])))
322
+ " "$line" 2>/dev/null)
323
+
324
+ if [[ -n "$completions" ]]; then
325
+ local first
326
+ first=$(echo "$completions" | head -n1)
327
+ if [[ "$first" != "$line" ]]; then
328
+ _SHELLMATE_TAB_CMD="$first"
329
+ _SHELLMATE_TAB_STATE="suggested"
330
+ local suffix="${first#$line}"
331
+ if [[ -n "$suffix" ]] && type ble/canvas/put.draw &>/dev/null; then
332
+ ble/canvas/put.draw "\e[90m${suffix}\e[m"
333
+ elif [[ -n "$suffix" ]]; then
334
+ printf '\e[90m%s\e[m' "$suffix" >/dev/tty
335
+ printf '\e[%dD' "${#suffix}" >/dev/tty
336
+ fi
337
+ fi
338
+ fi
339
+ }
340
+
341
+ # Bind hooks only when ble.sh is present
342
+ if type blehook &>/dev/null; then
343
+ ble-bind -f __default__ 'ble/widget/shellmate-on-keystroke' 2>/dev/null || true
344
+ ble-bind -f C-i 'ble/widget/shellmate-complete' 2>/dev/null || true # Tab = C-i
345
+ blehook PRECMD+=ble/widget/shellmate-check-catastrophic
346
+ blehook PREEXEC+=ble/widget/shellmate-before-exec
347
+ blehook PREEXEC+=ble/widget/shellmate-cancel-debounce
348
+ fi
349
+
350
+