npcsh 1.0.16__py3-none-any.whl → 1.0.18__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.
- npcsh/_state.py +1541 -78
- npcsh/corca.py +709 -0
- npcsh/guac.py +1433 -596
- npcsh/mcp_server.py +64 -60
- npcsh/npc.py +5 -4
- npcsh/npcsh.py +27 -1334
- npcsh/pti.py +195 -215
- npcsh/routes.py +99 -26
- npcsh/spool.py +138 -144
- npcsh-1.0.18.dist-info/METADATA +483 -0
- npcsh-1.0.18.dist-info/RECORD +21 -0
- {npcsh-1.0.16.dist-info → npcsh-1.0.18.dist-info}/entry_points.txt +1 -1
- npcsh/mcp_npcsh.py +0 -822
- npcsh-1.0.16.dist-info/METADATA +0 -825
- npcsh-1.0.16.dist-info/RECORD +0 -21
- {npcsh-1.0.16.dist-info → npcsh-1.0.18.dist-info}/WHEEL +0 -0
- {npcsh-1.0.16.dist-info → npcsh-1.0.18.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.0.16.dist-info → npcsh-1.0.18.dist-info}/top_level.txt +0 -0
npcsh/mcp_npcsh.py
DELETED
|
@@ -1,822 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# npcsh_mcp.py
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import atexit
|
|
7
|
-
import subprocess
|
|
8
|
-
import shlex
|
|
9
|
-
import re
|
|
10
|
-
from datetime import datetime
|
|
11
|
-
import argparse
|
|
12
|
-
import importlib.metadata
|
|
13
|
-
import textwrap
|
|
14
|
-
from typing import Optional, List, Dict, Any, Tuple, Union, Generator, Callable
|
|
15
|
-
try:
|
|
16
|
-
|
|
17
|
-
from inspect import isgenerator
|
|
18
|
-
|
|
19
|
-
except:
|
|
20
|
-
pass
|
|
21
|
-
import shutil
|
|
22
|
-
import asyncio
|
|
23
|
-
import json
|
|
24
|
-
from contextlib import AsyncExitStack
|
|
25
|
-
|
|
26
|
-
from termcolor import colored, cprint
|
|
27
|
-
import chromadb
|
|
28
|
-
|
|
29
|
-
from mcp import ClientSession, StdioServerParameters
|
|
30
|
-
from mcp.client.stdio import stdio_client
|
|
31
|
-
|
|
32
|
-
from npcsh._state import (
|
|
33
|
-
ShellState,
|
|
34
|
-
initial_state,
|
|
35
|
-
setup_npcsh_config,
|
|
36
|
-
is_npcsh_initialized,
|
|
37
|
-
initialize_base_npcs_if_needed,
|
|
38
|
-
orange,
|
|
39
|
-
interactive_commands,
|
|
40
|
-
BASH_COMMANDS,
|
|
41
|
-
log_action,
|
|
42
|
-
start_interactive_session,
|
|
43
|
-
)
|
|
44
|
-
from npcpy.npc_sysenv import (
|
|
45
|
-
print_and_process_stream_with_markdown,
|
|
46
|
-
render_markdown,
|
|
47
|
-
get_locally_available_models,
|
|
48
|
-
get_model_and_provider,
|
|
49
|
-
)
|
|
50
|
-
from npcpy.routes import router
|
|
51
|
-
from npcpy.data.image import capture_screenshot
|
|
52
|
-
from npcpy.memory.command_history import (
|
|
53
|
-
CommandHistory,
|
|
54
|
-
save_conversation_message,
|
|
55
|
-
)
|
|
56
|
-
from npcpy.npc_compiler import NPC, Team
|
|
57
|
-
from npcpy.gen.embeddings import get_embeddings
|
|
58
|
-
from npcpy.gen.response import get_litellm_response, get_ollama_response
|
|
59
|
-
from npcpy.llm_funcs import check_llm_command
|
|
60
|
-
|
|
61
|
-
import readline
|
|
62
|
-
|
|
63
|
-
VERSION = importlib.metadata.version("npcpy")
|
|
64
|
-
|
|
65
|
-
TERMINAL_EDITORS = ["vim", "emacs", "nano"]
|
|
66
|
-
EMBEDDINGS_DB_PATH = os.path.expanduser("~/npcsh_chroma.db")
|
|
67
|
-
HISTORY_DB_DEFAULT_PATH = os.path.expanduser("~/npcsh_history.db")
|
|
68
|
-
READLINE_HISTORY_FILE = os.path.expanduser("~/.npcsh_readline_history")
|
|
69
|
-
DEFAULT_NPC_TEAM_PATH = os.path.expanduser("~/.npcsh/npc_team/")
|
|
70
|
-
PROJECT_NPC_TEAM_PATH = "./npc_team/"
|
|
71
|
-
|
|
72
|
-
chroma_client = chromadb.PersistentClient(path=EMBEDDINGS_DB_PATH)
|
|
73
|
-
|
|
74
|
-
class CommandNotFoundError(Exception):
|
|
75
|
-
pass
|
|
76
|
-
|
|
77
|
-
class MCPClientNPC:
|
|
78
|
-
def __init__(self, debug: bool = True):
|
|
79
|
-
self.debug = debug
|
|
80
|
-
self.session: Optional[ClientSession] = None
|
|
81
|
-
self._exit_stack = asyncio.new_event_loop().run_until_complete(self._init_stack())
|
|
82
|
-
self.available_tools_llm: List[Dict[str, Any]] = []
|
|
83
|
-
self.tool_map: Dict[str, Callable] = {} # Map of tool names to functions
|
|
84
|
-
self.server_script_path: Optional[str] = None
|
|
85
|
-
|
|
86
|
-
async def _init_stack(self):
|
|
87
|
-
return AsyncExitStack()
|
|
88
|
-
|
|
89
|
-
def _log(self, message: str, color: str = "cyan") -> None:
|
|
90
|
-
if self.debug:
|
|
91
|
-
cprint(f"[MCP Client] {message}", color, file=sys.stderr)
|
|
92
|
-
|
|
93
|
-
async def _connect_async(self, server_script_path: str) -> None:
|
|
94
|
-
self._log(f"Attempting async connection to MCP server: {server_script_path}")
|
|
95
|
-
self.server_script_path = server_script_path
|
|
96
|
-
command_parts = []
|
|
97
|
-
abs_server_script_path = os.path.abspath(server_script_path)
|
|
98
|
-
if not os.path.exists(abs_server_script_path):
|
|
99
|
-
raise FileNotFoundError(f"MCP server script not found: {abs_server_script_path}")
|
|
100
|
-
|
|
101
|
-
if abs_server_script_path.endswith('.py'):
|
|
102
|
-
command_parts = [sys.executable, abs_server_script_path]
|
|
103
|
-
elif abs_server_script_path.endswith('.js'):
|
|
104
|
-
command_parts = ["node", abs_server_script_path]
|
|
105
|
-
elif os.access(abs_server_script_path, os.X_OK):
|
|
106
|
-
command_parts = [abs_server_script_path]
|
|
107
|
-
else:
|
|
108
|
-
raise ValueError(f"Unsupported MCP server script type or not executable: {abs_server_script_path}")
|
|
109
|
-
|
|
110
|
-
server_params = StdioServerParameters(command=command_parts[0], args=command_parts[1:], env=os.environ.copy())
|
|
111
|
-
if self.session:
|
|
112
|
-
await self._exit_stack.aclose()
|
|
113
|
-
self._exit_stack = AsyncExitStack()
|
|
114
|
-
|
|
115
|
-
stdio_transport = await self._exit_stack.enter_async_context(stdio_client(server_params))
|
|
116
|
-
read, write = stdio_transport
|
|
117
|
-
self.session = await self._exit_stack.enter_async_context(ClientSession(read, write))
|
|
118
|
-
await self.session.initialize()
|
|
119
|
-
|
|
120
|
-
# Get available tools from MCP server
|
|
121
|
-
response = await self.session.list_tools()
|
|
122
|
-
raw_tools_from_server = response.tools
|
|
123
|
-
|
|
124
|
-
# Reset our tool containers
|
|
125
|
-
self.available_tools_llm = []
|
|
126
|
-
self.tool_map = {}
|
|
127
|
-
|
|
128
|
-
# Process MCP tools
|
|
129
|
-
if raw_tools_from_server:
|
|
130
|
-
for mcp_tool_obj in raw_tools_from_server:
|
|
131
|
-
parameters_schema = getattr(mcp_tool_obj, "inputSchema", None)
|
|
132
|
-
if parameters_schema is None:
|
|
133
|
-
parameters_schema = {"type": "object", "properties": {}}
|
|
134
|
-
|
|
135
|
-
tool_name = mcp_tool_obj.name
|
|
136
|
-
|
|
137
|
-
# Create tool definition for LLM
|
|
138
|
-
tool_definition_for_llm = {
|
|
139
|
-
"type": "function",
|
|
140
|
-
"function": {
|
|
141
|
-
"name": tool_name,
|
|
142
|
-
"description": mcp_tool_obj.description or f"MCP tool named {tool_name}",
|
|
143
|
-
"parameters": parameters_schema
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
self.available_tools_llm.append(tool_definition_for_llm)
|
|
147
|
-
|
|
148
|
-
# Create a tool execution function and add to the tool map
|
|
149
|
-
# Use a closure to capture the correct tool name for each function
|
|
150
|
-
def make_tool_executor(t_name):
|
|
151
|
-
return lambda *args, **kwargs: self.execute_tool(t_name, kwargs if kwargs else args[0] if args else {})
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
self.tool_map[tool_name] = make_tool_executor(tool_name)
|
|
155
|
-
|
|
156
|
-
tool_names_for_log = [t['function']['name'] for t in self.available_tools_llm]
|
|
157
|
-
self._log(f"MCP Connection successful. Discovered and added {len(tool_names_for_log)} tools: {', '.join(tool_names_for_log) if tool_names_for_log else 'None'}")
|
|
158
|
-
|
|
159
|
-
def execute_tool(self, tool_name: str, args):
|
|
160
|
-
"""Execute an MCP tool using the session."""
|
|
161
|
-
if not self.session:
|
|
162
|
-
return {"error": "No active MCP session"}
|
|
163
|
-
|
|
164
|
-
# Ensure args is a dictionary
|
|
165
|
-
if not isinstance(args, dict):
|
|
166
|
-
args = args if isinstance(args, dict) else {}
|
|
167
|
-
|
|
168
|
-
# Special handling for lookup_provider - this is a built-in function, not an MCP tool
|
|
169
|
-
if tool_name == 'lookup_provider':
|
|
170
|
-
# Import the actual lookup_provider function
|
|
171
|
-
try:
|
|
172
|
-
# Call the actual function directly
|
|
173
|
-
model_name = args.get('model')
|
|
174
|
-
if model_name:
|
|
175
|
-
result = lookup_provider(model_name)
|
|
176
|
-
return {"provider": result, "model": model_name}
|
|
177
|
-
else:
|
|
178
|
-
return {"error": "Model name not provided for lookup_provider"}
|
|
179
|
-
except Exception as e:
|
|
180
|
-
return {"error": f"Error executing lookup_provider: {str(e)}"}
|
|
181
|
-
|
|
182
|
-
# For actual MCP tools, use the correct API
|
|
183
|
-
async def _execute_async():
|
|
184
|
-
try:
|
|
185
|
-
# The correct method to call an MCP tool might be different
|
|
186
|
-
# It could be something like:
|
|
187
|
-
result = await self.session.call_tool(tool_name, args)
|
|
188
|
-
# Or:
|
|
189
|
-
# result = await self.session.invoke_tool(tool_name, **args)
|
|
190
|
-
# Check your MCP client API documentation
|
|
191
|
-
return result
|
|
192
|
-
except Exception as e:
|
|
193
|
-
return {"error": f"Error executing MCP tool {tool_name}: {str(e)}"}
|
|
194
|
-
|
|
195
|
-
# Run the async function
|
|
196
|
-
loop = asyncio.get_event_loop_policy().get_event_loop()
|
|
197
|
-
if loop.is_closed():
|
|
198
|
-
loop = asyncio.new_event_loop()
|
|
199
|
-
asyncio.set_event_loop(loop)
|
|
200
|
-
return loop.run_until_complete(_execute_async())
|
|
201
|
-
|
|
202
|
-
def connect_to_server_sync(self, server_script_path: str) -> bool:
|
|
203
|
-
loop = asyncio.get_event_loop_policy().get_event_loop()
|
|
204
|
-
if loop.is_closed():
|
|
205
|
-
loop = asyncio.new_event_loop()
|
|
206
|
-
asyncio.set_event_loop(loop)
|
|
207
|
-
loop.run_until_complete(self._connect_async(server_script_path))
|
|
208
|
-
return True
|
|
209
|
-
|
|
210
|
-
async def _disconnect_async(self):
|
|
211
|
-
if self.session:
|
|
212
|
-
self._log("Disconnecting MCP session.")
|
|
213
|
-
await self._exit_stack.aclose()
|
|
214
|
-
self.session = None
|
|
215
|
-
self.available_tools_llm = []
|
|
216
|
-
self.tool_map = {}
|
|
217
|
-
self.server_script_path = None
|
|
218
|
-
else:
|
|
219
|
-
self._log("No active MCP session to disconnect.", "yellow")
|
|
220
|
-
|
|
221
|
-
def disconnect_sync(self):
|
|
222
|
-
loop = asyncio.get_event_loop_policy().get_event_loop()
|
|
223
|
-
if loop.is_closed():
|
|
224
|
-
loop = asyncio.new_event_loop()
|
|
225
|
-
asyncio.set_event_loop(loop)
|
|
226
|
-
loop.run_until_complete(self._disconnect_async())
|
|
227
|
-
self.session = None
|
|
228
|
-
self.available_tools_llm = []
|
|
229
|
-
self.tool_map = {}
|
|
230
|
-
|
|
231
|
-
def readline_safe_prompt(prompt: str) -> str:
|
|
232
|
-
ansi_escape = re.compile(r"(\033\[[0-9;]*[a-zA-Z])")
|
|
233
|
-
return ansi_escape.sub(r"\001\1\002", prompt)
|
|
234
|
-
|
|
235
|
-
def print_tools_info(tools_list: List[Dict[str, Any]]):
|
|
236
|
-
output = "Available tools:\n"
|
|
237
|
-
for tool_item in tools_list:
|
|
238
|
-
name = "UnknownTool"
|
|
239
|
-
description = "No description"
|
|
240
|
-
if isinstance(tool_item, dict):
|
|
241
|
-
if 'function' in tool_item and 'name' in tool_item['function']:
|
|
242
|
-
name = tool_item['function']['name']
|
|
243
|
-
if 'function' in tool_item and 'description' in tool_item['function']:
|
|
244
|
-
description = tool_item['function']['description']
|
|
245
|
-
output += f" {name}\n Description: {description}\n"
|
|
246
|
-
return output
|
|
247
|
-
|
|
248
|
-
def open_terminal_editor(command: str) -> str:
|
|
249
|
-
os.system(command)
|
|
250
|
-
return 'Terminal editor closed.'
|
|
251
|
-
|
|
252
|
-
def get_multiline_input(prompt: str) -> str:
|
|
253
|
-
lines = []
|
|
254
|
-
current_prompt = prompt
|
|
255
|
-
while True:
|
|
256
|
-
line = input(current_prompt)
|
|
257
|
-
if line.endswith("\\"):
|
|
258
|
-
lines.append(line[:-1])
|
|
259
|
-
current_prompt = readline_safe_prompt("> ")
|
|
260
|
-
else:
|
|
261
|
-
lines.append(line)
|
|
262
|
-
break
|
|
263
|
-
return "\n".join(lines)
|
|
264
|
-
|
|
265
|
-
def split_by_pipes(command: str) -> List[str]:
|
|
266
|
-
parts = []
|
|
267
|
-
current = ""
|
|
268
|
-
in_single_quote = False
|
|
269
|
-
in_double_quote = False
|
|
270
|
-
escape = False
|
|
271
|
-
for char in command:
|
|
272
|
-
if escape:
|
|
273
|
-
current += char
|
|
274
|
-
escape = False
|
|
275
|
-
elif char == '\\':
|
|
276
|
-
escape = True
|
|
277
|
-
current += char
|
|
278
|
-
elif char == "'" and not in_double_quote:
|
|
279
|
-
in_single_quote = not in_single_quote
|
|
280
|
-
current += char
|
|
281
|
-
elif char == '"' and not in_single_quote:
|
|
282
|
-
in_double_quote = not in_double_quote
|
|
283
|
-
current += char
|
|
284
|
-
elif char == '|' and not in_single_quote and not in_double_quote:
|
|
285
|
-
parts.append(current.strip())
|
|
286
|
-
current = ""
|
|
287
|
-
else:
|
|
288
|
-
current += char
|
|
289
|
-
if current:
|
|
290
|
-
parts.append(current.strip())
|
|
291
|
-
return parts
|
|
292
|
-
|
|
293
|
-
def parse_command_safely(cmd: str) -> List[str]:
|
|
294
|
-
return shlex.split(cmd)
|
|
295
|
-
|
|
296
|
-
def get_file_color(filepath: str) -> tuple:
|
|
297
|
-
if not os.path.exists(filepath):
|
|
298
|
-
return "grey", []
|
|
299
|
-
if os.path.isdir(filepath):
|
|
300
|
-
return "blue", ["bold"]
|
|
301
|
-
if os.access(filepath, os.X_OK) and not os.path.isdir(filepath):
|
|
302
|
-
return "green", ["bold"]
|
|
303
|
-
if filepath.endswith((".zip", ".tar", ".gz", ".bz2", ".xz", ".7z")):
|
|
304
|
-
return "red", []
|
|
305
|
-
if filepath.endswith((".py", ".pyw")):
|
|
306
|
-
return "yellow", []
|
|
307
|
-
return "white", []
|
|
308
|
-
|
|
309
|
-
def format_file_listing(output: str) -> str:
|
|
310
|
-
colored_lines = []
|
|
311
|
-
current_dir = os.getcwd()
|
|
312
|
-
for line in output.strip().split("\n"):
|
|
313
|
-
parts = line.split()
|
|
314
|
-
if not parts:
|
|
315
|
-
colored_lines.append(line)
|
|
316
|
-
continue
|
|
317
|
-
filepath_guess = parts[-1]
|
|
318
|
-
potential_path = os.path.join(current_dir, filepath_guess)
|
|
319
|
-
color, attrs = get_file_color(potential_path)
|
|
320
|
-
colored_filepath = colored(filepath_guess, color, attrs=attrs)
|
|
321
|
-
if len(parts) > 1 :
|
|
322
|
-
colored_line = " ".join(parts[:-1] + [colored_filepath])
|
|
323
|
-
else:
|
|
324
|
-
colored_line = colored_filepath
|
|
325
|
-
colored_lines.append(colored_line)
|
|
326
|
-
return "\n".join(colored_lines)
|
|
327
|
-
|
|
328
|
-
def wrap_text(text: str, width: int = 80) -> str:
|
|
329
|
-
lines = []
|
|
330
|
-
for paragraph in text.split("\n"):
|
|
331
|
-
if len(paragraph) > width:
|
|
332
|
-
lines.extend(textwrap.wrap(paragraph, width=width, replace_whitespace=False, drop_whitespace=False))
|
|
333
|
-
else:
|
|
334
|
-
lines.append(paragraph)
|
|
335
|
-
return "\n".join(lines)
|
|
336
|
-
|
|
337
|
-
def setup_readline_completer() -> str:
|
|
338
|
-
readline.read_history_file(READLINE_HISTORY_FILE)
|
|
339
|
-
readline.set_history_length(1000)
|
|
340
|
-
readline.parse_and_bind("set enable-bracketed-paste on")
|
|
341
|
-
readline.parse_and_bind(r'"\e[A": history-search-backward')
|
|
342
|
-
readline.parse_and_bind(r'"\e[B": history-search-forward')
|
|
343
|
-
if sys.platform == "darwin":
|
|
344
|
-
readline.parse_and_bind("bind ^I rl_complete")
|
|
345
|
-
else:
|
|
346
|
-
readline.parse_and_bind("tab: complete")
|
|
347
|
-
return READLINE_HISTORY_FILE
|
|
348
|
-
|
|
349
|
-
def save_readline_history_on_exit():
|
|
350
|
-
readline.write_history_file(READLINE_HISTORY_FILE)
|
|
351
|
-
|
|
352
|
-
valid_commands_list_global = list(router.routes.keys()) + list(interactive_commands.keys()) + ["cd", "exit", "quit"] + BASH_COMMANDS
|
|
353
|
-
|
|
354
|
-
def completer_func(text: str, state_idx: int) -> Optional[str]:
|
|
355
|
-
buffer = readline.get_line_buffer()
|
|
356
|
-
line_parts = parse_command_safely(buffer)
|
|
357
|
-
is_command_start = not line_parts or (len(line_parts) == 1 and not buffer.endswith(' '))
|
|
358
|
-
if is_command_start and not text.startswith('-'):
|
|
359
|
-
cmd_matches = [cmd + ' ' for cmd in valid_commands_list_global if cmd.startswith(text)]
|
|
360
|
-
if state_idx < len(cmd_matches):
|
|
361
|
-
return cmd_matches[state_idx]
|
|
362
|
-
else:
|
|
363
|
-
return None
|
|
364
|
-
if text and (not text.startswith('/') or os.path.exists(os.path.dirname(text))):
|
|
365
|
-
basedir = os.path.dirname(text)
|
|
366
|
-
prefix = os.path.basename(text)
|
|
367
|
-
search_dir = basedir if basedir else '.'
|
|
368
|
-
matches = [os.path.join(basedir, f) + ('/' if os.path.isdir(os.path.join(search_dir, f)) else ' ')
|
|
369
|
-
for f in os.listdir(search_dir) if f.startswith(prefix)]
|
|
370
|
-
if state_idx < len(matches):
|
|
371
|
-
return matches[state_idx]
|
|
372
|
-
else:
|
|
373
|
-
return None
|
|
374
|
-
return None
|
|
375
|
-
|
|
376
|
-
def store_command_embeddings(command: str, output: Any, state: ShellState):
|
|
377
|
-
if not chroma_client or not state.embedding_model or not state.embedding_provider:
|
|
378
|
-
return
|
|
379
|
-
if not command and not output:
|
|
380
|
-
return
|
|
381
|
-
output_str = str(output) if output else ""
|
|
382
|
-
if not command and not output_str:
|
|
383
|
-
return
|
|
384
|
-
texts_to_embed = [command, output_str]
|
|
385
|
-
embeddings = get_embeddings(texts_to_embed, state.embedding_model, state.embedding_provider)
|
|
386
|
-
if not embeddings or len(embeddings) != 2:
|
|
387
|
-
return
|
|
388
|
-
timestamp = datetime.now().isoformat()
|
|
389
|
-
npc_name = state.npc.name if isinstance(state.npc, NPC) else str(state.npc)
|
|
390
|
-
metadata = [{"type": "command", "timestamp": timestamp, "path": state.current_path, "npc": npc_name, "conversation_id": state.conversation_id},
|
|
391
|
-
{"type": "response", "timestamp": timestamp, "path": state.current_path, "npc": npc_name, "conversation_id": state.conversation_id}]
|
|
392
|
-
collection_name = f"{state.embedding_provider}_{state.embedding_model}_embeddings"
|
|
393
|
-
collection = chroma_client.get_or_create_collection(collection_name)
|
|
394
|
-
ids = [f"cmd_{timestamp}_{hash(command)}", f"resp_{timestamp}_{hash(output_str)}"]
|
|
395
|
-
collection.add(embeddings=embeddings, documents=texts_to_embed, metadatas=metadata, ids=ids)
|
|
396
|
-
|
|
397
|
-
def handle_interactive_command(cmd_parts: List[str], state: ShellState) -> Tuple[ShellState, str]:
|
|
398
|
-
command_name = cmd_parts[0]
|
|
399
|
-
print(f"Starting interactive {command_name} session...", file=sys.stderr)
|
|
400
|
-
return_code = start_interactive_session(interactive_commands[command_name], cmd_parts[1:])
|
|
401
|
-
output = f"Interactive {command_name} session ended with code {return_code}"
|
|
402
|
-
return state, output
|
|
403
|
-
|
|
404
|
-
def handle_cd_command(cmd_parts: List[str], state: ShellState) -> Tuple[ShellState, str]:
|
|
405
|
-
target_path = cmd_parts[1] if len(cmd_parts) > 1 else os.path.expanduser("~")
|
|
406
|
-
os.chdir(target_path)
|
|
407
|
-
state.current_path = os.getcwd()
|
|
408
|
-
output = f"Changed directory to {state.current_path}"
|
|
409
|
-
return state, output
|
|
410
|
-
|
|
411
|
-
def handle_bash_command(cmd_parts: List[str], cmd_str: str, stdin_input: Optional[str], state: ShellState) -> Tuple[ShellState, str]:
|
|
412
|
-
command_name = cmd_parts[0]
|
|
413
|
-
if command_name in TERMINAL_EDITORS:
|
|
414
|
-
return state, open_terminal_editor(cmd_str)
|
|
415
|
-
|
|
416
|
-
process = subprocess.Popen(
|
|
417
|
-
cmd_parts,
|
|
418
|
-
stdin=subprocess.PIPE if stdin_input is not None else None,
|
|
419
|
-
stdout=subprocess.PIPE,
|
|
420
|
-
stderr=subprocess.PIPE,
|
|
421
|
-
text=True,
|
|
422
|
-
cwd=state.current_path
|
|
423
|
-
)
|
|
424
|
-
stdout, stderr = process.communicate(input=stdin_input)
|
|
425
|
-
|
|
426
|
-
if process.returncode != 0:
|
|
427
|
-
err_msg = stderr.strip() if stderr else f"Command '{cmd_str}' failed (code {process.returncode})."
|
|
428
|
-
return state, (stdout.strip() + ("\n" + colored(f"stderr: {err_msg}", "red") if err_msg else "")).strip()
|
|
429
|
-
|
|
430
|
-
output = stdout.strip() if stdout else ""
|
|
431
|
-
if stderr:
|
|
432
|
-
print(colored(f"stderr: {stderr.strip()}", "yellow"), file=sys.stderr)
|
|
433
|
-
if command_name in ["ls", "find", "dir"]:
|
|
434
|
-
output = format_file_listing(output)
|
|
435
|
-
elif not output and process.returncode == 0 and not stderr:
|
|
436
|
-
output = ""
|
|
437
|
-
return state, output
|
|
438
|
-
|
|
439
|
-
def execute_slash_command_local(command: str, stdin_input: Optional[str], state: ShellState, stream: bool) -> Tuple[ShellState, Any]:
|
|
440
|
-
command_parts = command.split()
|
|
441
|
-
command_name = command_parts[0].lstrip('/')
|
|
442
|
-
|
|
443
|
-
handler = router.get_route(command_name)
|
|
444
|
-
if handler:
|
|
445
|
-
handler_kwargs = {'stream': stream, 'npc': state.npc, 'team': state.team, 'messages': state.messages,
|
|
446
|
-
'model': state.chat_model, 'provider': state.chat_provider, 'api_url': state.api_url, 'api_key': state.api_key}
|
|
447
|
-
if stdin_input is not None:
|
|
448
|
-
handler_kwargs['stdin_input'] = stdin_input
|
|
449
|
-
result_dict = handler(command, **handler_kwargs)
|
|
450
|
-
if isinstance(result_dict, dict):
|
|
451
|
-
output = result_dict.get("output") or result_dict.get("response")
|
|
452
|
-
state.messages = result_dict.get("messages", state.messages)
|
|
453
|
-
return state, output
|
|
454
|
-
else:
|
|
455
|
-
return state, result_dict
|
|
456
|
-
|
|
457
|
-
active_npc = state.npc if isinstance(state.npc, NPC) else None
|
|
458
|
-
if active_npc and command_name in active_npc.tools_dict:
|
|
459
|
-
return state, active_npc.tools_dict[command_name].run(*(command_parts[1:]), state=state, stdin_input=stdin_input, messages=state.messages)
|
|
460
|
-
|
|
461
|
-
if state.team and command_name in state.team.tools_dict:
|
|
462
|
-
return state, state.team.tools_dict[command_name].run(*(command_parts[1:]), state=state, stdin_input=stdin_input, messages=state.messages)
|
|
463
|
-
|
|
464
|
-
if state.team and command_name in state.team.npcs:
|
|
465
|
-
state.npc = state.team.npcs[command_name]
|
|
466
|
-
return state, f"Switched to NPC: {state.npc.name}"
|
|
467
|
-
|
|
468
|
-
return state, colored(f"Unknown slash command or tool: {command_name}", "red")
|
|
469
|
-
|
|
470
|
-
def process_pipeline_command(cmd_segment: str, stdin_input: Optional[str], state: ShellState, stream_final: bool) -> Tuple[ShellState, Any]:
|
|
471
|
-
if not cmd_segment:
|
|
472
|
-
return state, stdin_input
|
|
473
|
-
|
|
474
|
-
available_models_all = get_locally_available_models(state.current_path)
|
|
475
|
-
model_override, provider_override, cmd_cleaned = get_model_and_provider(cmd_segment, [i for _, i in available_models_all.items()])
|
|
476
|
-
cmd_to_process = cmd_cleaned.strip()
|
|
477
|
-
if not cmd_to_process:
|
|
478
|
-
return state, stdin_input
|
|
479
|
-
|
|
480
|
-
exec_model = model_override or state.chat_model
|
|
481
|
-
exec_provider = provider_override or state.chat_provider
|
|
482
|
-
|
|
483
|
-
if cmd_to_process.startswith("/"):
|
|
484
|
-
return execute_slash_command_local(cmd_to_process, stdin_input, state, stream_final)
|
|
485
|
-
|
|
486
|
-
cmd_parts = parse_command_safely(cmd_to_process)
|
|
487
|
-
if not cmd_parts:
|
|
488
|
-
return state, stdin_input
|
|
489
|
-
command_name = cmd_parts[0]
|
|
490
|
-
|
|
491
|
-
if command_name in interactive_commands:
|
|
492
|
-
return handle_interactive_command(cmd_parts, state)
|
|
493
|
-
|
|
494
|
-
if command_name == "cd":
|
|
495
|
-
return handle_cd_command(cmd_parts, state)
|
|
496
|
-
|
|
497
|
-
if command_name in BASH_COMMANDS:
|
|
498
|
-
return handle_bash_command(cmd_parts, cmd_to_process, stdin_input, state)
|
|
499
|
-
else:
|
|
500
|
-
full_llm_cmd = f"{cmd_to_process} {stdin_input}" if stdin_input else cmd_to_process
|
|
501
|
-
|
|
502
|
-
mcp_tools_for_upstream = None
|
|
503
|
-
if hasattr(state, 'mcp_client') and state.mcp_client and state.mcp_client.available_tools_llm:
|
|
504
|
-
mcp_tools_for_upstream = state.mcp_client.available_tools_llm
|
|
505
|
-
if hasattr(state.mcp_client, 'tool_map'):
|
|
506
|
-
mcp_tool_map = state.mcp_client.tool_map
|
|
507
|
-
|
|
508
|
-
result_dict = check_llm_command(
|
|
509
|
-
command=full_llm_cmd,
|
|
510
|
-
model=exec_model,
|
|
511
|
-
provider=exec_provider,
|
|
512
|
-
api_url=state.api_url,
|
|
513
|
-
api_key=state.api_key,
|
|
514
|
-
npc=state.npc,
|
|
515
|
-
team=state.team,
|
|
516
|
-
messages=state.messages,
|
|
517
|
-
images=state.attachments,
|
|
518
|
-
stream=stream_final,
|
|
519
|
-
tools=mcp_tools_for_upstream,
|
|
520
|
-
tool_map=mcp_tool_map,
|
|
521
|
-
|
|
522
|
-
context=state
|
|
523
|
-
)
|
|
524
|
-
state.messages = result_dict.get("messages", state.messages)
|
|
525
|
-
return state, result_dict.get("output")
|
|
526
|
-
|
|
527
|
-
def check_mode_switch(command:str , state: ShellState) -> Tuple[bool, ShellState]:
|
|
528
|
-
if command in ['/cmd', '/agent', '/chat', '/ride']:
|
|
529
|
-
state.current_mode = command[1:]
|
|
530
|
-
return True, state
|
|
531
|
-
return False, state
|
|
532
|
-
|
|
533
|
-
def execute_command(command: str, state: ShellState) -> Tuple[ShellState, Any]:
|
|
534
|
-
print(f'execute_command {command} {state.current_mode}', file=sys.stderr)
|
|
535
|
-
if not command.strip():
|
|
536
|
-
return state, ""
|
|
537
|
-
|
|
538
|
-
mode_change, state = check_mode_switch(command, state)
|
|
539
|
-
if mode_change:
|
|
540
|
-
return state, f'Mode changed to {state.current_mode}.'
|
|
541
|
-
|
|
542
|
-
original_command_for_embedding = command
|
|
543
|
-
|
|
544
|
-
if state.current_mode == 'agent':
|
|
545
|
-
commands = split_by_pipes(command)
|
|
546
|
-
stdin_for_next = None
|
|
547
|
-
final_output = None
|
|
548
|
-
current_state = state
|
|
549
|
-
for i, cmd_segment in enumerate(commands):
|
|
550
|
-
is_last_command = (i == len(commands) - 1)
|
|
551
|
-
stream_this_segment = is_last_command and state.stream_output
|
|
552
|
-
current_state, output = process_pipeline_command(cmd_segment.strip(), stdin_for_next, current_state, stream_this_segment)
|
|
553
|
-
if is_last_command:
|
|
554
|
-
final_output = output
|
|
555
|
-
if isinstance(output, str):
|
|
556
|
-
stdin_for_next = output
|
|
557
|
-
elif isgenerator(output):
|
|
558
|
-
if not stream_this_segment:
|
|
559
|
-
full_stream_output = "".join(map(str, output))
|
|
560
|
-
stdin_for_next = full_stream_output
|
|
561
|
-
if is_last_command:
|
|
562
|
-
final_output = full_stream_output
|
|
563
|
-
else:
|
|
564
|
-
stdin_for_next = None
|
|
565
|
-
final_output = output
|
|
566
|
-
elif output is not None:
|
|
567
|
-
stdin_for_next = str(output)
|
|
568
|
-
else:
|
|
569
|
-
stdin_for_next = None
|
|
570
|
-
if final_output is not None and not (isgenerator(final_output) and current_state.stream_output):
|
|
571
|
-
store_command_embeddings(original_command_for_embedding, final_output, current_state)
|
|
572
|
-
return current_state, final_output
|
|
573
|
-
|
|
574
|
-
elif state.current_mode == 'chat':
|
|
575
|
-
chat_messages = state.messages + [{"role": "user", "content": command}]
|
|
576
|
-
provider_to_use = state.chat_provider
|
|
577
|
-
if not provider_to_use and state.chat_model:
|
|
578
|
-
from npcpy.npc_sysenv import lookup_provider
|
|
579
|
-
provider_to_use = lookup_provider(state.chat_model)
|
|
580
|
-
|
|
581
|
-
if provider_to_use == "ollama":
|
|
582
|
-
response_dict = get_ollama_response(model=state.chat_model, messages=chat_messages, npc=state.npc, stream=state.stream_output, api_url=state.api_url)
|
|
583
|
-
else:
|
|
584
|
-
response_dict = get_litellm_response(model=state.chat_model, provider=provider_to_use, messages=chat_messages, npc=state.npc, stream=state.stream_output, api_key=state.api_key, api_url=state.api_url)
|
|
585
|
-
|
|
586
|
-
state.messages = response_dict.get("messages", state.messages)
|
|
587
|
-
return state, response_dict.get("response")
|
|
588
|
-
|
|
589
|
-
elif state.current_mode == 'cmd':
|
|
590
|
-
mcp_tools_for_upstream = None
|
|
591
|
-
if hasattr(state, 'mcp_client') and state.mcp_client and state.mcp_client.available_tools_llm:
|
|
592
|
-
mcp_tools_for_upstream = state.mcp_client.available_tools_llm
|
|
593
|
-
|
|
594
|
-
result_dict = check_llm_command(
|
|
595
|
-
command=command,
|
|
596
|
-
model=state.chat_model,
|
|
597
|
-
provider=state.chat_provider,
|
|
598
|
-
api_url=state.api_url,
|
|
599
|
-
api_key=state.api_key,
|
|
600
|
-
npc=state.npc,
|
|
601
|
-
team=state.team,
|
|
602
|
-
messages=state.messages,
|
|
603
|
-
images=state.attachments,
|
|
604
|
-
stream=state.stream_output,
|
|
605
|
-
tools=mcp_tools_for_upstream,
|
|
606
|
-
context=state
|
|
607
|
-
)
|
|
608
|
-
state.messages = result_dict.get("messages", state.messages)
|
|
609
|
-
return state, result_dict.get("output")
|
|
610
|
-
|
|
611
|
-
elif state.current_mode == 'ride':
|
|
612
|
-
print('Ride mode to be implemented soon', file=sys.stderr)
|
|
613
|
-
return state, None
|
|
614
|
-
|
|
615
|
-
return state, "Unknown execution mode."
|
|
616
|
-
|
|
617
|
-
def check_deprecation_warnings():
|
|
618
|
-
if os.getenv("NPCSH_MODEL"):
|
|
619
|
-
cprint("Deprecation Warning: NPCSH_MODEL/PROVIDER deprecated. Use NPCSH_CHAT_MODEL/PROVIDER.", "yellow", file=sys.stderr)
|
|
620
|
-
|
|
621
|
-
def print_welcome_message():
|
|
622
|
-
print("""
|
|
623
|
-
Welcome to \033[1;94mnpc\033[0m\033[1;38;5;202msh\033[0m (MCP Edition)!
|
|
624
|
-
\033[1;94m \033[0m\033[1;38;5;202m \\\\
|
|
625
|
-
\033[1;94m _ __ _ __ ___ \033[0m\033[1;38;5;202m ___ | |___ \\\\
|
|
626
|
-
\033[1;94m| '_ \\ | '_ \\ / __|\033[0m\033[1;38;5;202m/ __/ | |_ _| \\\\
|
|
627
|
-
\033[1;94m| | | || |_) |( |__ \033[0m\033[1;38;5;202m\_ \ | | | | //
|
|
628
|
-
\033[1;94m|_| |_|| .__/ \___|\033[0m\033[1;38;5;202m|___/ |_| |_| //
|
|
629
|
-
\033[1;94m| | \033[0m\033[1;38;5;202m //
|
|
630
|
-
\033[1;94m| |
|
|
631
|
-
\033[1;94m|_|
|
|
632
|
-
|
|
633
|
-
Type '/help' for commands. MCP server might be auto-connected.
|
|
634
|
-
""")
|
|
635
|
-
|
|
636
|
-
def setup_shell_mcp(cli_args: argparse.Namespace, shell_initial_state: ShellState) -> Tuple[CommandHistory, ShellState]:
|
|
637
|
-
check_deprecation_warnings()
|
|
638
|
-
setup_npcsh_config()
|
|
639
|
-
|
|
640
|
-
db_path = os.getenv("NPCSH_DB_PATH", HISTORY_DB_DEFAULT_PATH)
|
|
641
|
-
db_path = os.path.expanduser(db_path)
|
|
642
|
-
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
643
|
-
command_history = CommandHistory(db_path)
|
|
644
|
-
|
|
645
|
-
readline.set_completer(completer_func)
|
|
646
|
-
setup_readline_completer()
|
|
647
|
-
atexit.register(save_readline_history_on_exit)
|
|
648
|
-
atexit.register(command_history.close)
|
|
649
|
-
|
|
650
|
-
npc_directory = PROJECT_NPC_TEAM_PATH if os.path.exists(PROJECT_NPC_TEAM_PATH) else DEFAULT_NPC_TEAM_PATH
|
|
651
|
-
os.makedirs(npc_directory, exist_ok=True)
|
|
652
|
-
|
|
653
|
-
if not is_npcsh_initialized():
|
|
654
|
-
print("Initializing NPCSH...", file=sys.stderr)
|
|
655
|
-
initialize_base_npcs_if_needed(db_path)
|
|
656
|
-
print("NPCSH initialization complete. Restart or source ~/.npcshrc.", file=sys.stderr)
|
|
657
|
-
|
|
658
|
-
current_shell_state = shell_initial_state
|
|
659
|
-
|
|
660
|
-
team = Team(team_path=npc_directory)
|
|
661
|
-
current_shell_state.team = team
|
|
662
|
-
|
|
663
|
-
default_npc_name = getattr(team, 'default_npc_name', None)
|
|
664
|
-
if default_npc_name and default_npc_name in team.npcs:
|
|
665
|
-
current_shell_state.npc = team.npcs[default_npc_name]
|
|
666
|
-
elif team.npcs:
|
|
667
|
-
current_shell_state.npc = next(iter(team.npcs.values()))
|
|
668
|
-
cprint(f"No default NPC in team, using '{current_shell_state.npc.name}'.", "yellow", file=sys.stderr)
|
|
669
|
-
else:
|
|
670
|
-
sibiji_path = os.path.join(DEFAULT_NPC_TEAM_PATH, "sibiji.npc")
|
|
671
|
-
if os.path.exists(sibiji_path):
|
|
672
|
-
current_shell_state.npc = NPC(file=sibiji_path)
|
|
673
|
-
else:
|
|
674
|
-
cprint(f"Warning: No NPCs in team and 'sibiji.npc' not found.", "red", file=sys.stderr)
|
|
675
|
-
|
|
676
|
-
mcp_server_path = cli_args.mcp_server_path or os.getenv("NPCSH_MCP_SERVER_PATH")
|
|
677
|
-
if not mcp_server_path and current_shell_state.team:
|
|
678
|
-
team_mcp_servers = getattr(current_shell_state.team, 'mcp_servers', [])
|
|
679
|
-
default_mcp_name = getattr(current_shell_state.team, 'default_mcp_server_name', None)
|
|
680
|
-
processed_team_mcp_servers: List[Dict[str, str]] = []
|
|
681
|
-
if isinstance(team_mcp_servers, list):
|
|
682
|
-
for item in team_mcp_servers:
|
|
683
|
-
if isinstance(item, dict) and 'script_path' in item:
|
|
684
|
-
processed_team_mcp_servers.append({'name': item.get('name', os.path.basename(item['script_path'])), 'script_path': item['script_path']})
|
|
685
|
-
elif isinstance(item, str):
|
|
686
|
-
processed_team_mcp_servers.append({'name': os.path.basename(item), 'script_path': item})
|
|
687
|
-
|
|
688
|
-
if default_mcp_name:
|
|
689
|
-
for server_info in processed_team_mcp_servers:
|
|
690
|
-
if server_info.get('name') == default_mcp_name:
|
|
691
|
-
mcp_server_path = server_info.get('script_path')
|
|
692
|
-
break
|
|
693
|
-
elif len(processed_team_mcp_servers) == 1:
|
|
694
|
-
mcp_server_path = processed_team_mcp_servers[0].get('script_path')
|
|
695
|
-
|
|
696
|
-
if mcp_server_path and not os.path.isabs(mcp_server_path) and hasattr(current_shell_state.team, 'path') and current_shell_state.team.path:
|
|
697
|
-
mcp_server_path = os.path.join(current_shell_state.team.path, mcp_server_path)
|
|
698
|
-
|
|
699
|
-
if mcp_server_path:
|
|
700
|
-
cprint(f"Attempting to connect to MCP server: {mcp_server_path}", "yellow", file=sys.stderr)
|
|
701
|
-
mcp_client = MCPClientNPC()
|
|
702
|
-
if mcp_client.connect_to_server_sync(mcp_server_path):
|
|
703
|
-
if hasattr(current_shell_state, 'mcp_client'): # Check if the imported ShellState has the attr
|
|
704
|
-
current_shell_state.mcp_client = mcp_client
|
|
705
|
-
atexit.register(mcp_client.disconnect_sync)
|
|
706
|
-
else:
|
|
707
|
-
cprint("CRITICAL ERROR: Imported ShellState object from npcpy.modes._state does not have 'mcp_client' attribute.", "red", file=sys.stderr)
|
|
708
|
-
cprint("Please add 'mcp_client: Optional[Any] = None' to its definition.", "red", file=sys.stderr)
|
|
709
|
-
# Decide if script should exit here if mcp_client cannot be set on state
|
|
710
|
-
# For now, it will continue but mcp features might fail later if state.mcp_client is accessed
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if sys.stdin.isatty():
|
|
714
|
-
print_welcome_message()
|
|
715
|
-
return command_history, current_shell_state
|
|
716
|
-
|
|
717
|
-
def process_result(user_input: str, result_state: ShellState, output: Any, command_history: CommandHistory):
|
|
718
|
-
npc_name = result_state.npc.name if isinstance(result_state.npc, NPC) else str(result_state.npc)
|
|
719
|
-
team_name = result_state.team.name if isinstance(result_state.team, Team) else str(result_state.team)
|
|
720
|
-
save_conversation_message(command_history, result_state.conversation_id, "user", user_input,
|
|
721
|
-
wd=result_state.current_path, model=result_state.chat_model, provider=result_state.chat_provider,
|
|
722
|
-
npc=npc_name, team=team_name, attachments=result_state.attachments)
|
|
723
|
-
result_state.attachments = None
|
|
724
|
-
final_output_str = None
|
|
725
|
-
|
|
726
|
-
if result_state.stream_output and (isgenerator(output) or hasattr(output, '__aiter__')):
|
|
727
|
-
try:
|
|
728
|
-
final_output_str = print_and_process_stream_with_markdown(output, result_state.chat_model, result_state.chat_provider)
|
|
729
|
-
except AttributeError as e:
|
|
730
|
-
if isinstance(output, str):
|
|
731
|
-
if len(output) > 0:
|
|
732
|
-
final_output_str = output
|
|
733
|
-
render_markdown(final_output_str)
|
|
734
|
-
elif output is not None:
|
|
735
|
-
final_output_str = str(output)
|
|
736
|
-
render_markdown(final_output_str)
|
|
737
|
-
|
|
738
|
-
if final_output_str is not None:
|
|
739
|
-
is_new_assistant_message = not result_state.messages or result_state.messages[-1].get("role") != "assistant"
|
|
740
|
-
if is_new_assistant_message:
|
|
741
|
-
result_state.messages.append({"role": "assistant", "content": final_output_str})
|
|
742
|
-
elif result_state.messages[-1].get("content") is None:
|
|
743
|
-
result_state.messages[-1]["content"] = final_output_str
|
|
744
|
-
|
|
745
|
-
print()
|
|
746
|
-
if final_output_str:
|
|
747
|
-
save_conversation_message(command_history, result_state.conversation_id, "assistant", final_output_str,
|
|
748
|
-
wd=result_state.current_path, model=result_state.chat_model, provider=result_state.chat_provider,
|
|
749
|
-
npc=npc_name, team=team_name)
|
|
750
|
-
|
|
751
|
-
def run_repl(command_history: CommandHistory, shell_state_instance: ShellState):
|
|
752
|
-
state = shell_state_instance
|
|
753
|
-
print(f'Using {state.current_mode} mode. Use /agent, /cmd, or /chat to switch.', file=sys.stderr)
|
|
754
|
-
if state.npc:
|
|
755
|
-
print(f'Current NPC: {state.npc.name if isinstance(state.npc, NPC) else state.npc}', file=sys.stderr)
|
|
756
|
-
if hasattr(state, 'mcp_client') and state.mcp_client and state.mcp_client.server_script_path:
|
|
757
|
-
print(f'MCP Client connected to: {state.mcp_client.server_script_path}', file=sys.stderr)
|
|
758
|
-
elif hasattr(state, 'mcp_client') and state.mcp_client:
|
|
759
|
-
print(f'MCP Client active but not initially connected.', file=sys.stderr)
|
|
760
|
-
|
|
761
|
-
while True:
|
|
762
|
-
cwd_colored = colored(os.path.basename(state.current_path), "blue")
|
|
763
|
-
prompt_npc_part = f":{orange(state.npc.name)}" if isinstance(state.npc, NPC) else (f":{orange(str(state.npc))}" if state.npc else "")
|
|
764
|
-
prompt = readline_safe_prompt(f"{cwd_colored}{prompt_npc_part}> ")
|
|
765
|
-
user_input = get_multiline_input(prompt).strip()
|
|
766
|
-
if not user_input:
|
|
767
|
-
continue
|
|
768
|
-
if user_input.lower() in ["exit", "quit"]:
|
|
769
|
-
print("Goodbye!", file=sys.stderr)
|
|
770
|
-
break
|
|
771
|
-
state.current_path = os.getcwd()
|
|
772
|
-
state, output = execute_command(user_input, state)
|
|
773
|
-
process_result(user_input, state, output, command_history)
|
|
774
|
-
|
|
775
|
-
def run_non_interactive(command_history: CommandHistory, shell_state_instance: ShellState):
|
|
776
|
-
state = shell_state_instance
|
|
777
|
-
for line in sys.stdin:
|
|
778
|
-
user_input = line.strip()
|
|
779
|
-
if not user_input:
|
|
780
|
-
continue
|
|
781
|
-
if user_input.lower() in ["exit", "quit"]:
|
|
782
|
-
break
|
|
783
|
-
state.current_path = os.getcwd()
|
|
784
|
-
state, output = execute_command(user_input, state)
|
|
785
|
-
if state.stream_output and (isgenerator(output) or hasattr(output, '__aiter__')):
|
|
786
|
-
for chunk in output:
|
|
787
|
-
print(str(chunk), end='')
|
|
788
|
-
print()
|
|
789
|
-
elif output is not None:
|
|
790
|
-
print(output)
|
|
791
|
-
|
|
792
|
-
def main() -> None:
|
|
793
|
-
parser = argparse.ArgumentParser(description="npcsh (MCP Edition) - An NPC-powered shell with MCP.")
|
|
794
|
-
parser.add_argument("-v", "--version", action="version", version=f"npcsh_mcp version {VERSION}")
|
|
795
|
-
parser.add_argument("-c", "--command", type=str, help="Execute a single command and exit.")
|
|
796
|
-
parser.add_argument("--mcp-server-path", type=str, help="Path to an MCP server script to connect to.")
|
|
797
|
-
args = parser.parse_args()
|
|
798
|
-
|
|
799
|
-
command_history, shell_state_instance = setup_shell_mcp(args, initial_state)
|
|
800
|
-
|
|
801
|
-
if args.command:
|
|
802
|
-
state_for_cmd = shell_state_instance
|
|
803
|
-
state_for_cmd.current_path = os.getcwd()
|
|
804
|
-
final_state, output = execute_command(args.command, state_for_cmd)
|
|
805
|
-
if final_state.stream_output and (isgenerator(output) or hasattr(output, '__aiter__')):
|
|
806
|
-
for chunk in output:
|
|
807
|
-
print(str(chunk), end='')
|
|
808
|
-
print()
|
|
809
|
-
elif output is not None:
|
|
810
|
-
print(output)
|
|
811
|
-
elif not sys.stdin.isatty():
|
|
812
|
-
run_non_interactive(command_history, shell_state_instance)
|
|
813
|
-
else:
|
|
814
|
-
try:
|
|
815
|
-
run_repl(command_history, shell_state_instance)
|
|
816
|
-
except KeyboardInterrupt:
|
|
817
|
-
print("\nnpcsh terminated by user (KeyboardInterrupt).", file=sys.stderr)
|
|
818
|
-
except EOFError:
|
|
819
|
-
print("\nnpcsh terminated (EOF).", file=sys.stderr)
|
|
820
|
-
|
|
821
|
-
if __name__ == "__main__":
|
|
822
|
-
main()
|