ngpt 3.6.0__tar.gz → 3.7.0__tar.gz
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.
- {ngpt-3.6.0 → ngpt-3.7.0}/PKG-INFO +1 -1
- ngpt-3.7.0/ngpt/cli/modes/shell.py +674 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/pyproject.toml +1 -1
- {ngpt-3.6.0 → ngpt-3.7.0}/uv.lock +1 -1
- ngpt-3.6.0/ngpt/cli/modes/shell.py +0 -337
- {ngpt-3.6.0 → ngpt-3.7.0}/.github/workflows/aur-publish.yml +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/.github/workflows/python-publish.yml +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/.gitignore +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/.python-version +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/COMMIT_GUIDELINES.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/CONTRIBUTING.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/LICENSE +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/PKGBUILD +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/README.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/CONTRIBUTING.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/LICENSE.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/_config.yml +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/_sass/custom/custom.scss +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/configuration.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/examples/advanced.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/examples/basic.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/examples.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/index.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/installation.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/overview.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/usage/cli_config.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/usage/cli_usage.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/usage/gitcommsg.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/usage/web_search.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/docs/usage.md +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/__init__.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/__main__.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/__init__.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/args.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/config_manager.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/formatters.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/main.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/modes/__init__.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/modes/chat.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/modes/code.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/modes/gitcommsg.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/modes/interactive.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/modes/rewrite.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/modes/text.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/renderers.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/cli/ui.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/client.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/utils/__init__.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/utils/cli_config.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/utils/config.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/utils/log.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/ngpt/utils/web_search.py +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/previews/ngpt-g.png +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/previews/ngpt-i.png +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/previews/ngpt-s-c.png +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/previews/ngpt-sh-c-a.png +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/previews/ngpt-w-self.png +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/previews/ngpt-w.png +0 -0
- {ngpt-3.6.0 → ngpt-3.7.0}/wiki.md +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ngpt
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.7.0
|
4
4
|
Summary: Swiss army knife for LLMs: powerful CLI and interactive chatbot in one package. Seamlessly work with OpenAI, Ollama, Groq, Claude, Gemini, or any OpenAI-compatible API to generate code, craft git commits, rewrite text, and execute shell commands.
|
5
5
|
Project-URL: Homepage, https://github.com/nazdridoy/ngpt
|
6
6
|
Project-URL: Repository, https://github.com/nazdridoy/ngpt
|
@@ -0,0 +1,674 @@
|
|
1
|
+
from ..formatters import COLORS
|
2
|
+
from ..ui import spinner, copy_to_clipboard
|
3
|
+
from ..renderers import prettify_markdown, has_markdown_renderer, prettify_streaming_markdown, show_available_renderers
|
4
|
+
from ...utils import enhance_prompt_with_web_search
|
5
|
+
import subprocess
|
6
|
+
import sys
|
7
|
+
import threading
|
8
|
+
import platform
|
9
|
+
import os
|
10
|
+
import shutil
|
11
|
+
import re
|
12
|
+
import time
|
13
|
+
|
14
|
+
# System prompt for shell command generation
|
15
|
+
SHELL_SYSTEM_PROMPT = """Your role: Provide only plain text without Markdown formatting. Do not show any warnings or information regarding your capabilities. Do not provide any description. If you need to store any data, assume it will be stored in the chat. Provide only {shell_name} command for {operating_system} without any description. If there is a lack of details, provide most logical solution. Ensure the output is a valid shell command. If multiple steps required try to combine them together.
|
16
|
+
|
17
|
+
Command:"""
|
18
|
+
|
19
|
+
# System prompt to use when preprompt is provided
|
20
|
+
SHELL_PREPROMPT_TEMPLATE = """
|
21
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
22
|
+
!!! CRITICAL USER PREPROMPT !!!
|
23
|
+
!!! THIS OVERRIDES ALL OTHER INSTRUCTIONS INCLUDING OS/SHELL !!!
|
24
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
25
|
+
|
26
|
+
The following preprompt from the user COMPLETELY OVERRIDES ANY other instructions,
|
27
|
+
INCLUDING operating system type, shell type, or any other specifications below.
|
28
|
+
The preprompt MUST be followed EXACTLY AS WRITTEN:
|
29
|
+
|
30
|
+
>>> {preprompt} <<<
|
31
|
+
|
32
|
+
^^ THIS PREPROMPT HAS ABSOLUTE AND COMPLETE PRIORITY ^^
|
33
|
+
If the preprompt contradicts ANY OTHER instruction in this prompt,
|
34
|
+
including the {operating_system}/{shell_name} specification below,
|
35
|
+
YOU MUST FOLLOW THE PREPROMPT INSTRUCTION INSTEAD. NO EXCEPTIONS.
|
36
|
+
|
37
|
+
Your role: Provide only plain text without Markdown formatting. Do not show any warnings or information regarding your capabilities. Do not provide any description. If you need to store any data, assume it will be stored in the chat. Provide only {shell_name} command for {operating_system} without any description. If there is a lack of details, provide most logical solution. Ensure the output is a valid shell command. If multiple steps required try to combine them together.
|
38
|
+
|
39
|
+
Command:"""
|
40
|
+
|
41
|
+
def detect_shell():
|
42
|
+
"""Detect the current shell type and OS more accurately.
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
tuple: (shell_name, highlight_language, operating_system) - the detected shell name,
|
46
|
+
appropriate syntax highlighting language, and operating system
|
47
|
+
"""
|
48
|
+
os_type = platform.system()
|
49
|
+
|
50
|
+
# Determine OS with detailed information
|
51
|
+
if os_type == "Darwin":
|
52
|
+
operating_system = "MacOS"
|
53
|
+
elif os_type == "Linux":
|
54
|
+
# Try to get Linux distribution name
|
55
|
+
try:
|
56
|
+
result = subprocess.run(["lsb_release", "-si"], capture_output=True, text=True)
|
57
|
+
distro = result.stdout.strip()
|
58
|
+
operating_system = f"Linux/{distro}" if distro else "Linux"
|
59
|
+
except:
|
60
|
+
operating_system = "Linux"
|
61
|
+
elif os_type == "Windows":
|
62
|
+
operating_system = "Windows"
|
63
|
+
else:
|
64
|
+
operating_system = os_type
|
65
|
+
|
66
|
+
# Handle WSL specially - it looks like Linux but runs on Windows
|
67
|
+
is_wsl = False
|
68
|
+
try:
|
69
|
+
with open('/proc/version', 'r') as f:
|
70
|
+
if 'microsoft' in f.read().lower():
|
71
|
+
is_wsl = True
|
72
|
+
operating_system = "Windows/WSL"
|
73
|
+
except:
|
74
|
+
pass
|
75
|
+
|
76
|
+
# Try to detect the shell by examining environment variables
|
77
|
+
try:
|
78
|
+
# Check for specific shell by examining SHELL_NAME or equivalent
|
79
|
+
if os_type == "Windows" and not is_wsl:
|
80
|
+
# Check for Git Bash or MSYS2/Cygwin
|
81
|
+
if "MINGW" in os.environ.get("MSYSTEM", "") or "MSYS" in os.environ.get("MSYSTEM", ""):
|
82
|
+
return "bash", "bash", operating_system
|
83
|
+
|
84
|
+
# Check if we're in Git Bash by examining PATH for /mingw/
|
85
|
+
if any("/mingw/" in path.lower() for path in os.environ.get("PATH", "").split(os.pathsep)):
|
86
|
+
return "bash", "bash", operating_system
|
87
|
+
|
88
|
+
# Check for WSL within Windows
|
89
|
+
if "WSL" in os.environ.get("PATH", "") or "Microsoft" in os.environ.get("PATH", ""):
|
90
|
+
return "bash", "bash", operating_system
|
91
|
+
|
92
|
+
# Check for explicit shell path in environment
|
93
|
+
if os.environ.get("SHELL"):
|
94
|
+
shell_path = os.environ.get("SHELL").lower()
|
95
|
+
if "bash" in shell_path:
|
96
|
+
return "bash", "bash", operating_system
|
97
|
+
elif "zsh" in shell_path:
|
98
|
+
return "zsh", "zsh", operating_system
|
99
|
+
elif "powershell" in shell_path:
|
100
|
+
return "powershell.exe", "powershell", operating_system
|
101
|
+
elif "cmd" in shell_path:
|
102
|
+
return "cmd.exe", "batch", operating_system
|
103
|
+
|
104
|
+
# Check for PowerShell vs CMD
|
105
|
+
if os.environ.get("PSModulePath"):
|
106
|
+
# Further distinguish PowerShell vs PowerShell Core
|
107
|
+
if "pwsh" in os.environ.get("PSModulePath", "").lower():
|
108
|
+
return "pwsh", "powershell", operating_system
|
109
|
+
else:
|
110
|
+
return "powershell.exe", "powershell", operating_system
|
111
|
+
else:
|
112
|
+
return "cmd.exe", "batch", operating_system
|
113
|
+
else:
|
114
|
+
# Unix-like systems - try to get more specific
|
115
|
+
shell_path = os.environ.get("SHELL", "/bin/bash")
|
116
|
+
shell_name = os.path.basename(shell_path)
|
117
|
+
|
118
|
+
# Map shell name to syntax highlight language
|
119
|
+
if shell_name == "zsh":
|
120
|
+
return shell_name, "zsh", operating_system
|
121
|
+
elif shell_name == "fish":
|
122
|
+
return shell_name, "fish", operating_system
|
123
|
+
elif "csh" in shell_name:
|
124
|
+
return shell_name, "csh", operating_system
|
125
|
+
else:
|
126
|
+
# Default to bash for sh, bash, and other shells
|
127
|
+
return shell_name, "bash", operating_system
|
128
|
+
except Exception as e:
|
129
|
+
# Fall back to simple detection if anything fails
|
130
|
+
if os_type == "Windows":
|
131
|
+
return "powershell.exe", "powershell", operating_system
|
132
|
+
else:
|
133
|
+
return "bash", "bash", operating_system
|
134
|
+
|
135
|
+
def setup_streaming(args, logger=None):
|
136
|
+
"""Set up streaming configuration based on command-line arguments.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
args: The parsed command-line arguments
|
140
|
+
logger: Optional logger instance for logging
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
tuple: (should_stream, use_stream_prettify, use_regular_prettify,
|
144
|
+
stream_setup) - Configuration settings and streaming components
|
145
|
+
"""
|
146
|
+
# Default values - initialize all at once
|
147
|
+
stream_callback = live_display = stop_spinner_func = None
|
148
|
+
stop_spinner = spinner_thread = stop_spinner_event = None
|
149
|
+
should_stream = True # Default to streaming
|
150
|
+
use_stream_prettify = use_regular_prettify = False
|
151
|
+
first_content_received = False
|
152
|
+
|
153
|
+
# Determine final behavior based on flag priority
|
154
|
+
if args.stream_prettify:
|
155
|
+
# Highest priority: stream-prettify
|
156
|
+
if has_markdown_renderer('rich'):
|
157
|
+
should_stream = True
|
158
|
+
use_stream_prettify = True
|
159
|
+
live_display, stream_callback, setup_spinner = prettify_streaming_markdown(args.renderer)
|
160
|
+
if not live_display:
|
161
|
+
# Fallback if live display fails
|
162
|
+
use_stream_prettify = False
|
163
|
+
use_regular_prettify = True
|
164
|
+
should_stream = False
|
165
|
+
print(f"{COLORS['yellow']}Live display setup failed. Falling back to regular prettify mode.{COLORS['reset']}")
|
166
|
+
else:
|
167
|
+
# Rich not available for stream-prettify
|
168
|
+
print(f"{COLORS['yellow']}Warning: Rich is not available for --stream-prettify. Install with: pip install \"ngpt[full]\".{COLORS['reset']}")
|
169
|
+
print(f"{COLORS['yellow']}Falling back to default streaming without prettify.{COLORS['reset']}")
|
170
|
+
should_stream = True
|
171
|
+
use_stream_prettify = False
|
172
|
+
elif args.no_stream:
|
173
|
+
# Second priority: no-stream
|
174
|
+
should_stream = False
|
175
|
+
use_regular_prettify = False # No prettify if no streaming
|
176
|
+
elif args.prettify:
|
177
|
+
# Third priority: prettify (requires disabling stream)
|
178
|
+
if has_markdown_renderer(args.renderer):
|
179
|
+
should_stream = False
|
180
|
+
use_regular_prettify = True
|
181
|
+
print(f"{COLORS['yellow']}Note: Using standard markdown rendering (--prettify). For streaming markdown rendering, use --stream-prettify instead.{COLORS['reset']}")
|
182
|
+
else:
|
183
|
+
# Renderer not available for prettify
|
184
|
+
print(f"{COLORS['yellow']}Warning: Renderer '{args.renderer}' not available for --prettify.{COLORS['reset']}")
|
185
|
+
show_available_renderers()
|
186
|
+
print(f"{COLORS['yellow']}Falling back to default streaming without prettify.{COLORS['reset']}")
|
187
|
+
should_stream = True
|
188
|
+
use_regular_prettify = False
|
189
|
+
|
190
|
+
# Create a wrapper for the stream callback that will stop the spinner on first content
|
191
|
+
if stream_callback:
|
192
|
+
original_callback = stream_callback
|
193
|
+
|
194
|
+
def spinner_handling_callback(content, **kwargs):
|
195
|
+
nonlocal first_content_received
|
196
|
+
|
197
|
+
# On first content, stop the spinner
|
198
|
+
if not first_content_received and stop_spinner_func:
|
199
|
+
first_content_received = True
|
200
|
+
# Stop the spinner
|
201
|
+
stop_spinner_func()
|
202
|
+
# Ensure spinner message is cleared with an extra blank line
|
203
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
204
|
+
sys.stdout.flush()
|
205
|
+
|
206
|
+
# Call the original callback to update the display
|
207
|
+
if original_callback:
|
208
|
+
original_callback(content, **kwargs)
|
209
|
+
|
210
|
+
# Use our wrapper callback
|
211
|
+
if use_stream_prettify and live_display:
|
212
|
+
stream_callback = spinner_handling_callback
|
213
|
+
|
214
|
+
# Set up the spinner if we have a live display
|
215
|
+
stop_spinner_event = threading.Event()
|
216
|
+
stop_spinner_func = setup_spinner(stop_spinner_event, color=COLORS['cyan'])
|
217
|
+
|
218
|
+
# Create spinner for non-stream-prettify modes EXCEPT no-stream
|
219
|
+
if not use_stream_prettify and not args.no_stream:
|
220
|
+
# Prepare spinner (but don't start it yet - will be started in generate_with_model)
|
221
|
+
stop_spinner = threading.Event()
|
222
|
+
spinner_thread = threading.Thread(
|
223
|
+
target=spinner,
|
224
|
+
args=("Generating...",),
|
225
|
+
kwargs={"stop_event": stop_spinner, "color": COLORS['cyan']}
|
226
|
+
)
|
227
|
+
spinner_thread.daemon = True
|
228
|
+
|
229
|
+
# Create a stream_setup dict to hold all the variables - use a dict comprehension
|
230
|
+
stream_setup = {
|
231
|
+
'stream_callback': stream_callback,
|
232
|
+
'live_display': live_display,
|
233
|
+
'stop_spinner_func': stop_spinner_func,
|
234
|
+
'stop_spinner': stop_spinner,
|
235
|
+
'spinner_thread': spinner_thread,
|
236
|
+
'stop_spinner_event': stop_spinner_event,
|
237
|
+
'first_content_received': first_content_received
|
238
|
+
}
|
239
|
+
|
240
|
+
return (should_stream, use_stream_prettify, use_regular_prettify, stream_setup)
|
241
|
+
|
242
|
+
def generate_with_model(client, prompt, messages, args, stream_setup,
|
243
|
+
use_stream_prettify, should_stream, spinner_message="Generating...",
|
244
|
+
temp_override=None, logger=None):
|
245
|
+
"""Generate content using the model with proper streaming and spinner handling.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
client: The NGPTClient instance
|
249
|
+
prompt: The prompt to send to the model
|
250
|
+
messages: The formatted messages to send
|
251
|
+
args: The parsed command-line arguments
|
252
|
+
stream_setup: The streaming setup from setup_streaming
|
253
|
+
use_stream_prettify: Whether to use stream prettify
|
254
|
+
should_stream: Whether to stream the response
|
255
|
+
spinner_message: Message to show in the spinner
|
256
|
+
temp_override: Optional temperature override
|
257
|
+
logger: Optional logger instance
|
258
|
+
|
259
|
+
Returns:
|
260
|
+
str: The generated content
|
261
|
+
"""
|
262
|
+
# Extract variables from stream_setup - only unpack what we need
|
263
|
+
stream_callback = stream_setup['stream_callback']
|
264
|
+
stop_spinner = stream_setup['stop_spinner']
|
265
|
+
spinner_thread = stream_setup['spinner_thread']
|
266
|
+
stop_spinner_event = stream_setup['stop_spinner_event']
|
267
|
+
stop_spinner_func = stream_setup['stop_spinner_func']
|
268
|
+
|
269
|
+
# Show spinner for all modes except no-stream
|
270
|
+
if not args.no_stream:
|
271
|
+
# Two possible spinner types:
|
272
|
+
# 1. Rich spinner for stream_prettify
|
273
|
+
# 2. Regular spinner for all other modes (including --prettify)
|
274
|
+
|
275
|
+
if use_stream_prettify and stop_spinner_func:
|
276
|
+
# Rich spinner is handled by callbacks
|
277
|
+
pass
|
278
|
+
elif spinner_thread and stop_spinner:
|
279
|
+
# Start the regular spinner thread
|
280
|
+
spinner_thread._args = (spinner_message,)
|
281
|
+
if not spinner_thread.is_alive():
|
282
|
+
spinner_thread.start()
|
283
|
+
else:
|
284
|
+
# No-stream mode just gets a status message
|
285
|
+
print(spinner_message)
|
286
|
+
|
287
|
+
# Set temperature
|
288
|
+
temp = args.temperature if temp_override is None else temp_override
|
289
|
+
|
290
|
+
try:
|
291
|
+
# Make the API call
|
292
|
+
return client.chat(
|
293
|
+
prompt=prompt,
|
294
|
+
stream=should_stream,
|
295
|
+
messages=messages,
|
296
|
+
temperature=temp,
|
297
|
+
top_p=args.top_p,
|
298
|
+
max_tokens=args.max_tokens,
|
299
|
+
stream_callback=stream_callback
|
300
|
+
)
|
301
|
+
except KeyboardInterrupt:
|
302
|
+
print("\nRequest cancelled by user.")
|
303
|
+
return ""
|
304
|
+
except Exception as e:
|
305
|
+
print(f"Error generating content: {e}")
|
306
|
+
return ""
|
307
|
+
finally:
|
308
|
+
# Stop the spinner
|
309
|
+
if use_stream_prettify and stop_spinner_event:
|
310
|
+
# Stop rich spinner
|
311
|
+
if not stream_setup['first_content_received']:
|
312
|
+
stop_spinner_event.set()
|
313
|
+
elif stop_spinner:
|
314
|
+
# Stop regular spinner
|
315
|
+
stop_spinner.set()
|
316
|
+
if spinner_thread and spinner_thread.is_alive():
|
317
|
+
spinner_thread.join()
|
318
|
+
|
319
|
+
# Clear the spinner line completely
|
320
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
321
|
+
sys.stdout.flush()
|
322
|
+
|
323
|
+
def display_content(content, content_type, highlight_lang, args, use_stream_prettify, use_regular_prettify):
|
324
|
+
"""Display generated content with appropriate formatting.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
content: The content to display
|
328
|
+
content_type: Type of content ('command' or 'description')
|
329
|
+
highlight_lang: Language for syntax highlighting
|
330
|
+
args: The parsed command-line arguments
|
331
|
+
use_stream_prettify: Whether stream prettify is enabled
|
332
|
+
use_regular_prettify: Whether regular prettify is enabled
|
333
|
+
"""
|
334
|
+
if not content:
|
335
|
+
return
|
336
|
+
|
337
|
+
# Define title based on content type - use a lookup instead of if-else
|
338
|
+
titles = {
|
339
|
+
'command': "Generated Command",
|
340
|
+
'description': "Command Description"
|
341
|
+
}
|
342
|
+
title = titles.get(content_type, "Generated Content")
|
343
|
+
|
344
|
+
# Format content appropriately - create formatted content only when needed
|
345
|
+
if use_regular_prettify and has_markdown_renderer(args.renderer):
|
346
|
+
if content_type == 'command':
|
347
|
+
formatted_content = f"### {title}\n\n```{highlight_lang}\n{content}\n```"
|
348
|
+
else: # description
|
349
|
+
formatted_content = f"### {title}\n\n{content}"
|
350
|
+
|
351
|
+
# Only show formatted content if not already shown by stream-prettify
|
352
|
+
if not use_stream_prettify:
|
353
|
+
if use_regular_prettify and has_markdown_renderer(args.renderer):
|
354
|
+
# Use rich renderer for pretty output
|
355
|
+
prettify_markdown(formatted_content, args.renderer)
|
356
|
+
elif args.no_stream:
|
357
|
+
# Simple output for no-stream mode (no box)
|
358
|
+
if content_type == 'command':
|
359
|
+
print(f"\n{title}:\n{COLORS['green']}{content}{COLORS['reset']}\n")
|
360
|
+
else:
|
361
|
+
print(f"\n{title}:\n{content}\n")
|
362
|
+
else:
|
363
|
+
# Regular display or fallback
|
364
|
+
if content_type == 'command':
|
365
|
+
# Box formatting for commands in regular mode - calculate once
|
366
|
+
term_width = shutil.get_terminal_size().columns
|
367
|
+
box_width = min(term_width - 4, len(content) + 8)
|
368
|
+
horizontal_line = "─" * box_width
|
369
|
+
spacing = box_width - len(title) - 11
|
370
|
+
content_spacing = box_width - len(content) - 2
|
371
|
+
|
372
|
+
print(f"\n┌{horizontal_line}┐")
|
373
|
+
print(f"│ {COLORS['bold']}{title}:{COLORS['reset']} {' ' * spacing}│")
|
374
|
+
print(f"│ {COLORS['green']}{content}{COLORS['reset']}{' ' * content_spacing}│")
|
375
|
+
print(f"└{horizontal_line}┘\n")
|
376
|
+
else:
|
377
|
+
# Simple display for descriptions
|
378
|
+
print(f"\n{content}\n")
|
379
|
+
|
380
|
+
def shell_mode(client, args, logger=None):
|
381
|
+
"""Handle the shell command generation mode.
|
382
|
+
|
383
|
+
Args:
|
384
|
+
client: The NGPTClient instance
|
385
|
+
args: The parsed command-line arguments
|
386
|
+
logger: Optional logger instance
|
387
|
+
"""
|
388
|
+
# Get the user prompt more efficiently
|
389
|
+
if args.prompt is None:
|
390
|
+
try:
|
391
|
+
print("Enter shell command description: ", end='')
|
392
|
+
prompt = input()
|
393
|
+
except KeyboardInterrupt:
|
394
|
+
print("\nInput cancelled by user. Exiting gracefully.")
|
395
|
+
sys.exit(130)
|
396
|
+
else:
|
397
|
+
prompt = args.prompt
|
398
|
+
|
399
|
+
# Log the user prompt if logging is enabled
|
400
|
+
if logger:
|
401
|
+
logger.log("user", prompt)
|
402
|
+
|
403
|
+
# Enhance prompt with web search if enabled - reuse variables
|
404
|
+
if args.web_search:
|
405
|
+
original_prompt = prompt
|
406
|
+
web_search_succeeded = False
|
407
|
+
|
408
|
+
try:
|
409
|
+
# Start spinner for web search
|
410
|
+
stop_spinner = threading.Event()
|
411
|
+
spinner_thread = threading.Thread(
|
412
|
+
target=spinner,
|
413
|
+
args=("Searching the web for information...",),
|
414
|
+
kwargs={"stop_event": stop_spinner, "color": COLORS['cyan']}
|
415
|
+
)
|
416
|
+
spinner_thread.daemon = True
|
417
|
+
spinner_thread.start()
|
418
|
+
|
419
|
+
try:
|
420
|
+
prompt = enhance_prompt_with_web_search(prompt, logger=logger, disable_citations=True)
|
421
|
+
web_search_succeeded = True
|
422
|
+
finally:
|
423
|
+
# Always stop the spinner
|
424
|
+
stop_spinner.set()
|
425
|
+
spinner_thread.join()
|
426
|
+
|
427
|
+
# Clear the spinner line completely
|
428
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
429
|
+
sys.stdout.flush()
|
430
|
+
|
431
|
+
if web_search_succeeded:
|
432
|
+
print("Enhanced input with web search results.")
|
433
|
+
|
434
|
+
# Log the enhanced prompt if logging is enabled
|
435
|
+
if logger:
|
436
|
+
# Use "web_search" role instead of "system" for clearer logs
|
437
|
+
logger.log("web_search", prompt.replace(original_prompt, "").strip())
|
438
|
+
except Exception as e:
|
439
|
+
print(f"{COLORS['yellow']}Warning: Failed to enhance prompt with web search: {str(e)}{COLORS['reset']}")
|
440
|
+
# Continue with the original prompt if web search fails
|
441
|
+
|
442
|
+
# Detect shell type, highlight language, and operating system
|
443
|
+
shell_name, highlight_lang, operating_system = detect_shell()
|
444
|
+
|
445
|
+
# Format the system prompt based on whether preprompt is provided
|
446
|
+
if args.preprompt:
|
447
|
+
# Use the preprompt template with strong priority instructions
|
448
|
+
system_prompt = SHELL_PREPROMPT_TEMPLATE.format(
|
449
|
+
preprompt=args.preprompt,
|
450
|
+
operating_system=operating_system,
|
451
|
+
shell_name=shell_name
|
452
|
+
)
|
453
|
+
|
454
|
+
# Log the preprompt if logging is enabled
|
455
|
+
if logger:
|
456
|
+
logger.log("system", f"Preprompt: {args.preprompt}")
|
457
|
+
else:
|
458
|
+
# Use the normal system prompt with shell and OS information
|
459
|
+
system_prompt = SHELL_SYSTEM_PROMPT.format(
|
460
|
+
shell_name=shell_name,
|
461
|
+
operating_system=operating_system,
|
462
|
+
prompt=prompt
|
463
|
+
)
|
464
|
+
|
465
|
+
# Prepare messages for the chat API
|
466
|
+
messages = [
|
467
|
+
{"role": "system", "content": system_prompt},
|
468
|
+
{"role": "user", "content": prompt}
|
469
|
+
]
|
470
|
+
|
471
|
+
# Log the system prompt if logging is enabled
|
472
|
+
if logger:
|
473
|
+
logger.log("system", system_prompt)
|
474
|
+
|
475
|
+
# Set up streaming once and reuse for both command and description
|
476
|
+
should_stream, use_stream_prettify, use_regular_prettify, stream_setup = setup_streaming(args)
|
477
|
+
|
478
|
+
# Generate the command
|
479
|
+
command = generate_with_model(
|
480
|
+
client=client,
|
481
|
+
prompt=prompt,
|
482
|
+
messages=messages,
|
483
|
+
args=args,
|
484
|
+
stream_setup=stream_setup,
|
485
|
+
use_stream_prettify=use_stream_prettify,
|
486
|
+
should_stream=should_stream,
|
487
|
+
spinner_message="Generating command...",
|
488
|
+
logger=logger
|
489
|
+
)
|
490
|
+
|
491
|
+
if not command:
|
492
|
+
return # Error already printed by client
|
493
|
+
|
494
|
+
# Log the generated command if logging is enabled
|
495
|
+
if logger:
|
496
|
+
logger.log("assistant", command)
|
497
|
+
|
498
|
+
# Get the most up-to-date shell type at command generation time
|
499
|
+
_, highlight_lang, _ = detect_shell()
|
500
|
+
|
501
|
+
# Format with proper syntax highlighting for streaming prettify - only if needed
|
502
|
+
if use_stream_prettify and stream_setup['stream_callback'] and command:
|
503
|
+
# Create properly formatted markdown for streaming display
|
504
|
+
formatted_command = f"```{highlight_lang}\n{command}\n```"
|
505
|
+
# Update the live display with the formatted command
|
506
|
+
stream_setup['stream_callback'](formatted_command, complete=True)
|
507
|
+
|
508
|
+
# Display the command
|
509
|
+
display_content(
|
510
|
+
content=command,
|
511
|
+
content_type='command',
|
512
|
+
highlight_lang=highlight_lang,
|
513
|
+
args=args,
|
514
|
+
use_stream_prettify=use_stream_prettify,
|
515
|
+
use_regular_prettify=use_regular_prettify
|
516
|
+
)
|
517
|
+
|
518
|
+
# Display options with better formatting - prepare strings once
|
519
|
+
options_text = f"{COLORS['bold']}Options:{COLORS['reset']}"
|
520
|
+
options = [
|
521
|
+
f" {COLORS['cyan']}C{COLORS['reset']} - Copy - Copy the command to clipboard",
|
522
|
+
f" {COLORS['cyan']}E{COLORS['reset']} - Execute - Run the command in your shell",
|
523
|
+
f" {COLORS['cyan']}D{COLORS['reset']} - Describe - Explain what this command does",
|
524
|
+
f" {COLORS['cyan']}A{COLORS['reset']} - Abort - Cancel and return to prompt"
|
525
|
+
]
|
526
|
+
prompt_text = f"\nWhat would you like to do? [{COLORS['cyan']}C{COLORS['reset']}/{COLORS['cyan']}E{COLORS['reset']}/{COLORS['cyan']}D{COLORS['reset']}/{COLORS['cyan']}A{COLORS['reset']}] "
|
527
|
+
|
528
|
+
# Print options with proper flushing to ensure display
|
529
|
+
print(options_text, flush=True)
|
530
|
+
for option in options:
|
531
|
+
print(option, flush=True)
|
532
|
+
|
533
|
+
# Add a small delay to ensure terminal rendering is complete,
|
534
|
+
# especially important for stream-prettify mode
|
535
|
+
if use_stream_prettify:
|
536
|
+
time.sleep(0.2)
|
537
|
+
|
538
|
+
# Print prompt and flush to ensure it appears
|
539
|
+
print(prompt_text, end='', flush=True)
|
540
|
+
|
541
|
+
try:
|
542
|
+
response = input().lower()
|
543
|
+
except KeyboardInterrupt:
|
544
|
+
print("\nCommand execution cancelled by user.")
|
545
|
+
return
|
546
|
+
|
547
|
+
if response == 'e':
|
548
|
+
# Log the execution if logging is enabled
|
549
|
+
if logger:
|
550
|
+
logger.log("system", f"Executing command: {command}")
|
551
|
+
|
552
|
+
try:
|
553
|
+
try:
|
554
|
+
print("\nExecuting command... (Press Ctrl+C to cancel)")
|
555
|
+
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
556
|
+
output = result.stdout
|
557
|
+
|
558
|
+
# Log the command output if logging is enabled
|
559
|
+
if logger:
|
560
|
+
logger.log("system", f"Command output: {output}")
|
561
|
+
|
562
|
+
print(f"\nOutput:\n{output}")
|
563
|
+
except KeyboardInterrupt:
|
564
|
+
print("\nCommand execution cancelled by user.")
|
565
|
+
|
566
|
+
# Log the cancellation if logging is enabled
|
567
|
+
if logger:
|
568
|
+
logger.log("system", "Command execution cancelled by user")
|
569
|
+
except subprocess.CalledProcessError as e:
|
570
|
+
error = e.stderr
|
571
|
+
|
572
|
+
# Log the error if logging is enabled
|
573
|
+
if logger:
|
574
|
+
logger.log("system", f"Command error: {error}")
|
575
|
+
|
576
|
+
print(f"\nError:\n{error}")
|
577
|
+
elif response == 'c':
|
578
|
+
# Copy command to clipboard without confirmation prompt
|
579
|
+
copied = copy_to_clipboard(command, skip_confirmation=True)
|
580
|
+
if not copied:
|
581
|
+
print(f"{COLORS['yellow']}Failed to copy to clipboard. Command: {COLORS['green']}{command}{COLORS['reset']}")
|
582
|
+
|
583
|
+
# Log the copy if logging is enabled
|
584
|
+
if logger:
|
585
|
+
logger.log("system", "Command copied to clipboard")
|
586
|
+
elif response == 'd':
|
587
|
+
# Ask LLM to describe what the command does
|
588
|
+
describe_prompt = f"Please explain this command: {command}"
|
589
|
+
|
590
|
+
# Create system prompt for description that includes OS and shell info
|
591
|
+
describe_system_prompt = f"You are a helpful assistant explaining shell commands. The user is running {shell_name} on {operating_system}. Explain what the following shell command does in detail, considering this specific environment. Include any potential risks, side effects, or compatibility issues with this OS/shell combination."
|
592
|
+
|
593
|
+
# Prepare messages for the chat API
|
594
|
+
describe_messages = [
|
595
|
+
{"role": "system", "content": describe_system_prompt},
|
596
|
+
{"role": "user", "content": describe_prompt}
|
597
|
+
]
|
598
|
+
|
599
|
+
# Log the system prompt if logging is enabled
|
600
|
+
if logger:
|
601
|
+
logger.log("system", f"Command description requested for {operating_system}/{shell_name}")
|
602
|
+
|
603
|
+
# Set up fresh streaming for description - reuse existing setup when possible
|
604
|
+
# We only need to refresh the streaming setup if we're using stream_prettify
|
605
|
+
if use_stream_prettify:
|
606
|
+
_, use_stream_prettify_desc, use_regular_prettify_desc, stream_setup_desc = setup_streaming(args)
|
607
|
+
else:
|
608
|
+
# Reuse the existing setup for non-prettify streaming
|
609
|
+
use_stream_prettify_desc = use_stream_prettify
|
610
|
+
use_regular_prettify_desc = use_regular_prettify
|
611
|
+
|
612
|
+
# Always create a fresh spinner for description
|
613
|
+
stop_spinner = threading.Event()
|
614
|
+
spinner_thread = threading.Thread(
|
615
|
+
target=spinner,
|
616
|
+
args=("Generating command description...",),
|
617
|
+
kwargs={"stop_event": stop_spinner, "color": COLORS['cyan']}
|
618
|
+
)
|
619
|
+
spinner_thread.daemon = True
|
620
|
+
|
621
|
+
# Create a new stream setup with the fresh spinner
|
622
|
+
stream_setup_desc = {
|
623
|
+
'stream_callback': stream_setup.get('stream_callback'),
|
624
|
+
'live_display': stream_setup.get('live_display'),
|
625
|
+
'stop_spinner_func': stream_setup.get('stop_spinner_func'),
|
626
|
+
'stop_spinner': stop_spinner,
|
627
|
+
'spinner_thread': spinner_thread,
|
628
|
+
'stop_spinner_event': stream_setup.get('stop_spinner_event'),
|
629
|
+
'first_content_received': False
|
630
|
+
}
|
631
|
+
|
632
|
+
# Generate the description
|
633
|
+
description = generate_with_model(
|
634
|
+
client=client,
|
635
|
+
prompt=describe_prompt,
|
636
|
+
messages=describe_messages,
|
637
|
+
args=args,
|
638
|
+
stream_setup=stream_setup_desc,
|
639
|
+
use_stream_prettify=use_stream_prettify_desc,
|
640
|
+
should_stream=should_stream,
|
641
|
+
spinner_message="Generating command description...",
|
642
|
+
temp_override=0.3,
|
643
|
+
logger=logger
|
644
|
+
)
|
645
|
+
|
646
|
+
if not description:
|
647
|
+
return # Error already printed
|
648
|
+
|
649
|
+
# Log the generated description if logging is enabled
|
650
|
+
if logger:
|
651
|
+
logger.log("assistant", description)
|
652
|
+
|
653
|
+
# Format with proper markdown for streaming prettify - only if needed
|
654
|
+
if use_stream_prettify_desc and stream_setup_desc['stream_callback'] and description:
|
655
|
+
# Format description as markdown for prettier display
|
656
|
+
md_description = f"### Command Description\n\n{description}"
|
657
|
+
# Update the live display with the formatted description
|
658
|
+
stream_setup_desc['stream_callback'](md_description, complete=True)
|
659
|
+
|
660
|
+
# Display the description
|
661
|
+
display_content(
|
662
|
+
content=description,
|
663
|
+
content_type='description',
|
664
|
+
highlight_lang=highlight_lang,
|
665
|
+
args=args,
|
666
|
+
use_stream_prettify=use_stream_prettify_desc,
|
667
|
+
use_regular_prettify=use_regular_prettify_desc
|
668
|
+
)
|
669
|
+
elif response == 'a' or response == '':
|
670
|
+
print("\nCommand aborted.")
|
671
|
+
|
672
|
+
# Log the abort if logging is enabled
|
673
|
+
if logger:
|
674
|
+
logger.log("system", "Command aborted by user")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "ngpt"
|
3
|
-
version = "3.
|
3
|
+
version = "3.7.0"
|
4
4
|
description = "Swiss army knife for LLMs: powerful CLI and interactive chatbot in one package. Seamlessly work with OpenAI, Ollama, Groq, Claude, Gemini, or any OpenAI-compatible API to generate code, craft git commits, rewrite text, and execute shell commands."
|
5
5
|
authors = [
|
6
6
|
{name = "nazDridoy", email = "nazdridoy399@gmail.com"},
|
@@ -1,337 +0,0 @@
|
|
1
|
-
from ..formatters import COLORS
|
2
|
-
from ..ui import spinner, copy_to_clipboard
|
3
|
-
from ..renderers import prettify_markdown, has_markdown_renderer
|
4
|
-
from ...utils import enhance_prompt_with_web_search
|
5
|
-
import subprocess
|
6
|
-
import sys
|
7
|
-
import threading
|
8
|
-
import platform
|
9
|
-
import os
|
10
|
-
import shutil
|
11
|
-
import re
|
12
|
-
|
13
|
-
# System prompt for shell command generation
|
14
|
-
SHELL_SYSTEM_PROMPT = """Your role: Provide only plain text without Markdown formatting. Do not show any warnings or information regarding your capabilities. Do not provide any description. If you need to store any data, assume it will be stored in the chat. Provide only {shell_name} command for {operating_system} without any description. If there is a lack of details, provide most logical solution. Ensure the output is a valid shell command. If multiple steps required try to combine them together.
|
15
|
-
|
16
|
-
Command:"""
|
17
|
-
|
18
|
-
# System prompt to use when preprompt is provided
|
19
|
-
SHELL_PREPROMPT_TEMPLATE = """
|
20
|
-
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
21
|
-
!!! CRITICAL USER PREPROMPT !!!
|
22
|
-
!!! THIS OVERRIDES ALL OTHER INSTRUCTIONS INCLUDING OS/SHELL !!!
|
23
|
-
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
24
|
-
|
25
|
-
The following preprompt from the user COMPLETELY OVERRIDES ANY other instructions,
|
26
|
-
INCLUDING operating system type, shell type, or any other specifications below.
|
27
|
-
The preprompt MUST be followed EXACTLY AS WRITTEN:
|
28
|
-
|
29
|
-
>>> {preprompt} <<<
|
30
|
-
|
31
|
-
^^ THIS PREPROMPT HAS ABSOLUTE AND COMPLETE PRIORITY ^^
|
32
|
-
If the preprompt contradicts ANY OTHER instruction in this prompt,
|
33
|
-
including the {operating_system}/{shell_name} specification below,
|
34
|
-
YOU MUST FOLLOW THE PREPROMPT INSTRUCTION INSTEAD. NO EXCEPTIONS.
|
35
|
-
|
36
|
-
Your role: Provide only plain text without Markdown formatting. Do not show any warnings or information regarding your capabilities. Do not provide any description. If you need to store any data, assume it will be stored in the chat. Provide only {shell_name} command for {operating_system} without any description. If there is a lack of details, provide most logical solution. Ensure the output is a valid shell command. If multiple steps required try to combine them together.
|
37
|
-
|
38
|
-
Command:"""
|
39
|
-
|
40
|
-
def detect_shell():
|
41
|
-
"""Detect the current shell type and OS more accurately.
|
42
|
-
|
43
|
-
Returns:
|
44
|
-
tuple: (shell_name, highlight_language, operating_system) - the detected shell name,
|
45
|
-
appropriate syntax highlighting language, and operating system
|
46
|
-
"""
|
47
|
-
os_type = platform.system()
|
48
|
-
|
49
|
-
# Determine OS with detailed information
|
50
|
-
if os_type == "Darwin":
|
51
|
-
operating_system = "MacOS"
|
52
|
-
elif os_type == "Linux":
|
53
|
-
# Try to get Linux distribution name
|
54
|
-
try:
|
55
|
-
result = subprocess.run(["lsb_release", "-si"], capture_output=True, text=True)
|
56
|
-
distro = result.stdout.strip()
|
57
|
-
operating_system = f"Linux/{distro}" if distro else "Linux"
|
58
|
-
except:
|
59
|
-
operating_system = "Linux"
|
60
|
-
elif os_type == "Windows":
|
61
|
-
operating_system = "Windows"
|
62
|
-
else:
|
63
|
-
operating_system = os_type
|
64
|
-
|
65
|
-
# Handle WSL specially - it looks like Linux but runs on Windows
|
66
|
-
is_wsl = False
|
67
|
-
try:
|
68
|
-
with open('/proc/version', 'r') as f:
|
69
|
-
if 'microsoft' in f.read().lower():
|
70
|
-
is_wsl = True
|
71
|
-
operating_system = "Windows/WSL"
|
72
|
-
except:
|
73
|
-
pass
|
74
|
-
|
75
|
-
# Try to detect the shell by examining environment variables
|
76
|
-
try:
|
77
|
-
# Check for specific shell by examining SHELL_NAME or equivalent
|
78
|
-
if os_type == "Windows" and not is_wsl:
|
79
|
-
# Check for Git Bash or MSYS2/Cygwin
|
80
|
-
if "MINGW" in os.environ.get("MSYSTEM", "") or "MSYS" in os.environ.get("MSYSTEM", ""):
|
81
|
-
return "bash", "bash", operating_system
|
82
|
-
|
83
|
-
# Check if we're in Git Bash by examining PATH for /mingw/
|
84
|
-
if any("/mingw/" in path.lower() for path in os.environ.get("PATH", "").split(os.pathsep)):
|
85
|
-
return "bash", "bash", operating_system
|
86
|
-
|
87
|
-
# Check for WSL within Windows
|
88
|
-
if "WSL" in os.environ.get("PATH", "") or "Microsoft" in os.environ.get("PATH", ""):
|
89
|
-
return "bash", "bash", operating_system
|
90
|
-
|
91
|
-
# Check for explicit shell path in environment
|
92
|
-
if os.environ.get("SHELL"):
|
93
|
-
shell_path = os.environ.get("SHELL").lower()
|
94
|
-
if "bash" in shell_path:
|
95
|
-
return "bash", "bash", operating_system
|
96
|
-
elif "zsh" in shell_path:
|
97
|
-
return "zsh", "zsh", operating_system
|
98
|
-
elif "powershell" in shell_path:
|
99
|
-
return "powershell.exe", "powershell", operating_system
|
100
|
-
elif "cmd" in shell_path:
|
101
|
-
return "cmd.exe", "batch", operating_system
|
102
|
-
|
103
|
-
# Check for PowerShell vs CMD
|
104
|
-
if os.environ.get("PSModulePath"):
|
105
|
-
# Further distinguish PowerShell vs PowerShell Core
|
106
|
-
if "pwsh" in os.environ.get("PSModulePath", "").lower():
|
107
|
-
return "pwsh", "powershell", operating_system
|
108
|
-
else:
|
109
|
-
return "powershell.exe", "powershell", operating_system
|
110
|
-
else:
|
111
|
-
return "cmd.exe", "batch", operating_system
|
112
|
-
else:
|
113
|
-
# Unix-like systems - try to get more specific
|
114
|
-
shell_path = os.environ.get("SHELL", "/bin/bash")
|
115
|
-
shell_name = os.path.basename(shell_path)
|
116
|
-
|
117
|
-
# Map shell name to syntax highlight language
|
118
|
-
if shell_name == "zsh":
|
119
|
-
return shell_name, "zsh", operating_system
|
120
|
-
elif shell_name == "fish":
|
121
|
-
return shell_name, "fish", operating_system
|
122
|
-
elif "csh" in shell_name:
|
123
|
-
return shell_name, "csh", operating_system
|
124
|
-
else:
|
125
|
-
# Default to bash for sh, bash, and other shells
|
126
|
-
return shell_name, "bash", operating_system
|
127
|
-
except Exception as e:
|
128
|
-
# Fall back to simple detection if anything fails
|
129
|
-
if os_type == "Windows":
|
130
|
-
return "powershell.exe", "powershell", operating_system
|
131
|
-
else:
|
132
|
-
return "bash", "bash", operating_system
|
133
|
-
|
134
|
-
def shell_mode(client, args, logger=None):
|
135
|
-
"""Handle the shell command generation mode.
|
136
|
-
|
137
|
-
Args:
|
138
|
-
client: The NGPTClient instance
|
139
|
-
args: The parsed command-line arguments
|
140
|
-
logger: Optional logger instance
|
141
|
-
"""
|
142
|
-
if args.prompt is None:
|
143
|
-
try:
|
144
|
-
print("Enter shell command description: ", end='')
|
145
|
-
prompt = input()
|
146
|
-
except KeyboardInterrupt:
|
147
|
-
print("\nInput cancelled by user. Exiting gracefully.")
|
148
|
-
sys.exit(130)
|
149
|
-
else:
|
150
|
-
prompt = args.prompt
|
151
|
-
|
152
|
-
# Log the user prompt if logging is enabled
|
153
|
-
if logger:
|
154
|
-
logger.log("user", prompt)
|
155
|
-
|
156
|
-
# Enhance prompt with web search if enabled
|
157
|
-
if args.web_search:
|
158
|
-
try:
|
159
|
-
original_prompt = prompt
|
160
|
-
|
161
|
-
# Start spinner for web search
|
162
|
-
stop_spinner = threading.Event()
|
163
|
-
spinner_thread = threading.Thread(
|
164
|
-
target=spinner,
|
165
|
-
args=("Searching the web for information...",),
|
166
|
-
kwargs={"stop_event": stop_spinner, "color": COLORS['cyan']}
|
167
|
-
)
|
168
|
-
spinner_thread.daemon = True
|
169
|
-
spinner_thread.start()
|
170
|
-
|
171
|
-
try:
|
172
|
-
prompt = enhance_prompt_with_web_search(prompt, logger=logger, disable_citations=True)
|
173
|
-
# Stop the spinner
|
174
|
-
stop_spinner.set()
|
175
|
-
spinner_thread.join()
|
176
|
-
# Clear the spinner line completely
|
177
|
-
sys.stdout.write("\r" + " " * 100 + "\r")
|
178
|
-
sys.stdout.flush()
|
179
|
-
print("Enhanced input with web search results.")
|
180
|
-
except Exception as e:
|
181
|
-
# Stop the spinner before re-raising
|
182
|
-
stop_spinner.set()
|
183
|
-
spinner_thread.join()
|
184
|
-
raise e
|
185
|
-
|
186
|
-
# Log the enhanced prompt if logging is enabled
|
187
|
-
if logger:
|
188
|
-
# Use "web_search" role instead of "system" for clearer logs
|
189
|
-
logger.log("web_search", prompt.replace(original_prompt, "").strip())
|
190
|
-
except Exception as e:
|
191
|
-
print(f"{COLORS['yellow']}Warning: Failed to enhance prompt with web search: {str(e)}{COLORS['reset']}")
|
192
|
-
# Continue with the original prompt if web search fails
|
193
|
-
|
194
|
-
# Detect shell type, highlight language, and operating system
|
195
|
-
shell_name, highlight_lang, operating_system = detect_shell()
|
196
|
-
|
197
|
-
# Format the system prompt based on whether preprompt is provided
|
198
|
-
if args.preprompt:
|
199
|
-
# Use the preprompt template with strong priority instructions
|
200
|
-
system_prompt = SHELL_PREPROMPT_TEMPLATE.format(
|
201
|
-
preprompt=args.preprompt,
|
202
|
-
operating_system=operating_system,
|
203
|
-
shell_name=shell_name
|
204
|
-
)
|
205
|
-
|
206
|
-
# Log the preprompt if logging is enabled
|
207
|
-
if logger:
|
208
|
-
logger.log("system", f"Preprompt: {args.preprompt}")
|
209
|
-
else:
|
210
|
-
# Use the normal system prompt with shell and OS information
|
211
|
-
system_prompt = SHELL_SYSTEM_PROMPT.format(
|
212
|
-
shell_name=shell_name,
|
213
|
-
operating_system=operating_system,
|
214
|
-
prompt=prompt
|
215
|
-
)
|
216
|
-
|
217
|
-
# Prepare messages for the chat API
|
218
|
-
messages = [
|
219
|
-
{"role": "system", "content": system_prompt},
|
220
|
-
{"role": "user", "content": prompt}
|
221
|
-
]
|
222
|
-
|
223
|
-
# Log the system prompt if logging is enabled
|
224
|
-
if logger:
|
225
|
-
logger.log("system", system_prompt)
|
226
|
-
|
227
|
-
# Start spinner while waiting for command generation
|
228
|
-
stop_spinner = threading.Event()
|
229
|
-
spinner_thread = threading.Thread(
|
230
|
-
target=spinner,
|
231
|
-
args=("Generating command...",),
|
232
|
-
kwargs={"stop_event": stop_spinner, "color": COLORS['cyan']}
|
233
|
-
)
|
234
|
-
spinner_thread.daemon = True
|
235
|
-
spinner_thread.start()
|
236
|
-
|
237
|
-
try:
|
238
|
-
command = client.chat(
|
239
|
-
prompt=prompt,
|
240
|
-
stream=False,
|
241
|
-
messages=messages,
|
242
|
-
temperature=args.temperature,
|
243
|
-
top_p=args.top_p,
|
244
|
-
max_tokens=args.max_tokens
|
245
|
-
)
|
246
|
-
except Exception as e:
|
247
|
-
print(f"Error generating shell command: {e}")
|
248
|
-
command = ""
|
249
|
-
finally:
|
250
|
-
# Stop the spinner
|
251
|
-
stop_spinner.set()
|
252
|
-
spinner_thread.join()
|
253
|
-
|
254
|
-
# Clear the spinner line completely
|
255
|
-
sys.stdout.write("\r" + " " * 100 + "\r")
|
256
|
-
sys.stdout.flush()
|
257
|
-
|
258
|
-
if not command:
|
259
|
-
return # Error already printed by client
|
260
|
-
|
261
|
-
# Log the generated command if logging is enabled
|
262
|
-
if logger:
|
263
|
-
logger.log("assistant", command)
|
264
|
-
|
265
|
-
# Get the most up-to-date shell type at command generation time
|
266
|
-
_, highlight_lang, _ = detect_shell()
|
267
|
-
|
268
|
-
# Create a markdown-formatted display of the command with appropriate syntax highlighting
|
269
|
-
formatted_command = f"### Generated Command\n\n```{highlight_lang}\n{command}\n```"
|
270
|
-
|
271
|
-
# Check if we can use rich rendering
|
272
|
-
if has_markdown_renderer('rich'):
|
273
|
-
# Use rich renderer for pretty output
|
274
|
-
prettify_markdown(formatted_command, 'rich')
|
275
|
-
else:
|
276
|
-
# Fallback to simpler formatting
|
277
|
-
term_width = shutil.get_terminal_size().columns
|
278
|
-
box_width = min(term_width - 4, len(command) + 8)
|
279
|
-
horizontal_line = "─" * box_width
|
280
|
-
|
281
|
-
print(f"\n┌{horizontal_line}┐")
|
282
|
-
print(f"│ {COLORS['bold']}Generated Command:{COLORS['reset']} {' ' * (box_width - 20)}│")
|
283
|
-
print(f"│ {COLORS['green']}{command}{COLORS['reset']}{' ' * (box_width - len(command) - 2)}│")
|
284
|
-
print(f"└{horizontal_line}┘\n")
|
285
|
-
|
286
|
-
# Display options with better formatting
|
287
|
-
print(f"{COLORS['bold']}Options:{COLORS['reset']}")
|
288
|
-
print(f" {COLORS['cyan']}c{COLORS['reset']} - Copy to clipboard")
|
289
|
-
print(f" {COLORS['cyan']}e{COLORS['reset']} - Execute command")
|
290
|
-
print(f" {COLORS['cyan']}n{COLORS['reset']} - Cancel (default)")
|
291
|
-
print(f"\nWhat would you like to do? [{COLORS['cyan']}c{COLORS['reset']}/{COLORS['cyan']}e{COLORS['reset']}/N] ", end='')
|
292
|
-
|
293
|
-
try:
|
294
|
-
response = input().lower()
|
295
|
-
except KeyboardInterrupt:
|
296
|
-
print("\nCommand execution cancelled by user.")
|
297
|
-
return
|
298
|
-
|
299
|
-
if response == 'e' or response == 'execute':
|
300
|
-
# Log the execution if logging is enabled
|
301
|
-
if logger:
|
302
|
-
logger.log("system", f"Executing command: {command}")
|
303
|
-
|
304
|
-
try:
|
305
|
-
try:
|
306
|
-
print("\nExecuting command... (Press Ctrl+C to cancel)")
|
307
|
-
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
308
|
-
output = result.stdout
|
309
|
-
|
310
|
-
# Log the command output if logging is enabled
|
311
|
-
if logger:
|
312
|
-
logger.log("system", f"Command output: {output}")
|
313
|
-
|
314
|
-
print(f"\nOutput:\n{output}")
|
315
|
-
except KeyboardInterrupt:
|
316
|
-
print("\nCommand execution cancelled by user.")
|
317
|
-
|
318
|
-
# Log the cancellation if logging is enabled
|
319
|
-
if logger:
|
320
|
-
logger.log("system", "Command execution cancelled by user")
|
321
|
-
except subprocess.CalledProcessError as e:
|
322
|
-
error = e.stderr
|
323
|
-
|
324
|
-
# Log the error if logging is enabled
|
325
|
-
if logger:
|
326
|
-
logger.log("system", f"Command error: {error}")
|
327
|
-
|
328
|
-
print(f"\nError:\n{error}")
|
329
|
-
elif response == 'c' or response == 'copy':
|
330
|
-
# Copy command to clipboard without confirmation prompt
|
331
|
-
copied = copy_to_clipboard(command, skip_confirmation=True)
|
332
|
-
if not copied:
|
333
|
-
print(f"{COLORS['yellow']}Failed to copy to clipboard. Command: {COLORS['green']}{command}{COLORS['reset']}")
|
334
|
-
|
335
|
-
# Log the copy if logging is enabled
|
336
|
-
if logger:
|
337
|
-
logger.log("system", "Command copied to clipboard")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|