npcsh 1.0.14__py3-none-any.whl → 1.0.17__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/mcp_server.py CHANGED
@@ -14,7 +14,6 @@ from typing import Optional, Dict, Any, List, Union, Callable
14
14
  from mcp.server.fastmcp import FastMCP
15
15
  import importlib
16
16
  # npcpy imports
17
- from npcpy.gen.response import get_litellm_response
18
17
 
19
18
 
20
19
  import os
@@ -28,15 +27,27 @@ except:
28
27
  from typing import Optional, Dict, Any, List, Union, Callable, get_type_hints
29
28
  # Add these imports to the top of your file
30
29
  from functools import wraps
30
+ import sys
31
+
32
+ from npcpy.llm_funcs import generate_group_candidates, abstract, extract_facts, zoom_in, execute_llm_command, gen_image
33
+ from npcpy.memory.search import search_similar_texts, execute_search_command, execute_rag_command, answer_with_rag, execute_brainblast_command
34
+ from npcpy.data.load import load_file_contents
35
+ from npcpy.memory.command_history import CommandHistory
36
+ from npcpy.data.image import capture_screenshot
37
+ from npcpy.data.web import search_web
38
+
39
+ from npcsh._state import NPCSH_DB_PATH
40
+
41
+ command_history = CommandHistory(db=NPCSH_DB_PATH)
42
+
31
43
  # Initialize the MCP server
32
- mcp = FastMCP("npcpy_enhanced")
44
+ mcp = FastMCP("npcsh_mcp")
33
45
 
34
46
  # Define the default workspace
35
47
  DEFAULT_WORKSPACE = os.path.join(os.getcwd(), "workspace")
36
48
  os.makedirs(DEFAULT_WORKSPACE, exist_ok=True)
37
49
 
38
50
  # ==================== SYSTEM TOOLS ====================
39
-
40
51
  @mcp.tool()
41
52
  async def run_server_command(command: str) -> str:
42
53
  """
@@ -54,45 +65,44 @@ async def run_server_command(command: str) -> str:
54
65
  cwd=DEFAULT_WORKSPACE,
55
66
  shell=True,
56
67
  capture_output=True,
57
- text=True
68
+ text=True,
69
+ timeout=30 # Add timeout to prevent hanging
58
70
  )
59
- return result.stdout or result.stderr
71
+ return result.stdout or result.stderr or "Command completed with no output"
72
+ except subprocess.TimeoutExpired:
73
+ return "Command timed out after 30 seconds"
60
74
  except Exception as e:
61
75
  return str(e)
76
+
77
+
78
+
62
79
  def make_async_wrapper(func: Callable) -> Callable:
63
- """Create an async wrapper for sync functions that fixes schema validation issues."""
80
+ """Create an async wrapper for sync functions."""
64
81
 
65
82
  @wraps(func)
66
- async def async_wrapper(*args, **kwargs):
67
- # Direct parameter dict case (most common failure)
68
- if len(args) == 1 and isinstance(args[0], dict):
69
- params = args[0]
70
-
71
- # Fix for search_web - add required kwargs parameter
72
- if "kwargs" not in params:
73
- # Create a new dict with the kwargs parameter added
74
- params = {**params, "kwargs": ""}
75
-
76
- # Call the function with the parameters
77
- if asyncio.iscoroutinefunction(func):
78
- return await func(**params)
79
- else:
80
- return await asyncio.to_thread(func, **params)
83
+ async def async_wrapper(**kwargs):
84
+ func_name = func.__name__
85
+ print(f"MCP SERVER DEBUG: {func_name} called with kwargs={kwargs}", flush=True)
81
86
 
82
- # Normal function call or other cases
83
- if asyncio.iscoroutinefunction(func):
84
- return await func(*args, **kwargs)
85
- else:
86
- return await asyncio.to_thread(func, *args, **kwargs)
87
+ try:
88
+ result = func(**kwargs)
89
+ print(f"MCP SERVER DEBUG: {func_name} returned type={type(result)}, result={result[:500] if isinstance(result, str) else result}", flush=True)
90
+ return result
91
+
92
+ except Exception as e:
93
+ print(f"MCP SERVER DEBUG: {func_name} exception: {e}", flush=True)
94
+ import traceback
95
+ traceback.print_exc()
96
+ return f"Error in {func_name}: {e}"
87
97
 
88
- # Preserve function metadata
89
98
  async_wrapper.__name__ = func.__name__
90
99
  async_wrapper.__doc__ = func.__doc__
91
100
  async_wrapper.__annotations__ = func.__annotations__
92
101
 
93
102
  return async_wrapper
94
103
 
95
- # Update your register_module_tools function to use this improved wrapper
104
+
105
+
96
106
  def register_module_tools(module_name: str) -> None:
97
107
  """
98
108
  Register all suitable functions from a module as MCP tools with improved argument handling.
@@ -133,45 +143,39 @@ def load_module_functions(module_name: str) -> List[Callable]:
133
143
 
134
144
  print("Loading tools from npcpy modules...")
135
145
 
136
- # Load modules from npcpy.routes
137
- try:
138
- from npcpy.routes import routes
139
- for route_name, route_func in routes.items():
140
- if callable(route_func):
141
- async_func = make_async_wrapper(route_func)
142
- try:
143
- mcp.tool()(async_func)
144
- print(f"Registered route: {route_name}")
145
- except Exception as e:
146
- print(f"Failed to register route {route_name}: {e}")
147
- except ImportError as e:
148
- print(f"Warning: Could not import routes: {e}")
149
146
 
150
147
 
151
- # Load npc_compiler functions
152
- print("Loading functions from npcpy.npc_compiler...")
153
- try:
154
- import importlib.util
155
- if importlib.util.find_spec("npcpy.npc_compiler"):
156
- register_module_tools("npcpy.npc_compiler")
157
- except ImportError:
158
- print("npcpy.npc_compiler not found, skipping...")
159
148
 
160
- # Load npc_sysenv functions
161
- #print("Loading functions from npcpy.npc_sysenv...")
162
- #register_module_tools("npcpy.npc_sysenv")
163
- register_module_tools("npcpy.memory.search")
164
149
 
165
- register_module_tools("npcpy.work.plan")
166
- register_module_tools("npcpy.work.trigger")
167
- register_module_tools("npcpy.work.desktop")
150
+ def register_selected_npcpy_tools():
151
+ tools = [generate_group_candidates,
152
+ abstract,
153
+ extract_facts,
154
+ zoom_in,
155
+ execute_llm_command,
156
+ gen_image,
157
+ load_file_contents,
158
+ capture_screenshot,
159
+ search_web, ]
168
160
 
169
- #print("Loading functions from npcpy.command_history...")
170
- #register_module_tools("npcpy.memory.command_history")
161
+ for func in tools:
162
+ # Ensure a docstring exists for schema generation
163
+ if not (getattr(func, "__doc__", None) and func.__doc__.strip()):
164
+ fallback_doc = f"Tool wrapper for {func.__name__}."
165
+ try:
166
+ func.__doc__ = fallback_doc
167
+ except Exception:
168
+ pass # Some builtins may not allow setting __doc__
169
+
170
+ try:
171
+ async_func = make_async_wrapper(func)
172
+ mcp.tool()(async_func)
173
+ print(f"Registered npcpy tool: {func.__name__}")
174
+ except Exception as e:
175
+ print(f"Failed to register npcpy tool {func.__name__}: {e}")
176
+ register_selected_npcpy_tools()
171
177
 
172
178
 
173
- #print("Loading functions from npcpy.npc_sysenv...")
174
- #register_module_tools("npcpy.llm_funcs")
175
179
 
176
180
 
177
181
  # ==================== MAIN ENTRY POINT ====================
npcsh/npc.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import argparse
2
2
  import sys
3
3
  import os
4
- import sqlite3
5
4
  import traceback
6
5
  from typing import Optional
7
6
 
@@ -11,7 +10,8 @@ from npcsh._state import (
11
10
  NPCSH_API_URL,
12
11
  NPCSH_DB_PATH,
13
12
  NPCSH_STREAM_OUTPUT,
14
- )
13
+ initial_state,
14
+ )
15
15
  from npcpy.npc_sysenv import (
16
16
  print_and_process_stream_with_markdown,
17
17
  render_markdown,
@@ -19,8 +19,17 @@ from npcpy.npc_sysenv import (
19
19
  from npcpy.npc_compiler import NPC, Team
20
20
  from npcsh.routes import router
21
21
  from npcpy.llm_funcs import check_llm_command
22
+ from sqlalchemy import create_engine
23
+
24
+ # Import the key functions from npcsh
25
+ from npcsh._state import (
26
+ setup_shell,
27
+ execute_slash_command,
28
+ execute_command,
29
+ )
22
30
 
23
31
  def load_npc_by_name(npc_name: str = "sibiji", db_path: str = NPCSH_DB_PATH) -> Optional[NPC]:
32
+ """Load NPC by name, with fallback logic matching npcsh"""
24
33
  if not npc_name:
25
34
  npc_name = "sibiji"
26
35
 
@@ -37,7 +46,7 @@ def load_npc_by_name(npc_name: str = "sibiji", db_path: str = NPCSH_DB_PATH) ->
37
46
 
38
47
  if chosen_path:
39
48
  try:
40
- db_conn = sqlite3.connect(db_path)
49
+ db_conn = create_engine(f'sqlite:///{NPCSH_DB_PATH}')
41
50
  npc = NPC(file=chosen_path, db_conn=db_conn)
42
51
  return npc
43
52
  except Exception as e:
@@ -50,6 +59,8 @@ def load_npc_by_name(npc_name: str = "sibiji", db_path: str = NPCSH_DB_PATH) ->
50
59
  return None
51
60
 
52
61
  def main():
62
+ from npcsh.routes import router
63
+
53
64
  parser = argparse.ArgumentParser(
54
65
  description="NPC Command Line Utilities. Call a command or provide a prompt for the default NPC.",
55
66
  usage="npc <command> [command_args...] | <prompt> [--npc NAME] [--model MODEL] [--provider PROV]"
@@ -64,34 +75,38 @@ def main():
64
75
  "-n", "--npc", help="Name of the NPC to use (default: sibiji)", type=str, default="sibiji"
65
76
  )
66
77
 
67
- # No subparsers setup at first - we'll conditionally create them
68
-
69
- # First, get any arguments without parsing commands
78
+ # Parse arguments
70
79
  args, all_args = parser.parse_known_args()
71
80
  global_model = args.model
72
81
  global_provider = args.provider
73
82
 
74
- # Check if the first argument is a known command
75
83
  is_valid_command = False
76
84
  command_name = None
77
- if all_args and all_args[0] in router.get_commands():
78
- is_valid_command = True
79
- command_name = all_args[0]
80
- all_args = all_args[1:] # Remove the command from arguments
85
+
86
+ if all_args:
87
+ first_arg = all_args[0]
88
+ if first_arg.startswith('/'):
89
+ is_valid_command = True
90
+ command_name = first_arg
91
+ all_args = all_args[1:]
92
+ elif first_arg in router.get_commands():
93
+ is_valid_command = True
94
+ command_name = '/' + first_arg
95
+ all_args = all_args[1:]
96
+
97
+
81
98
 
82
- # Only set up subparsers if we have a valid command
83
99
  if is_valid_command:
84
100
  subparsers = parser.add_subparsers(dest="command", title="Available Commands",
85
101
  help="Run 'npc <command> --help' for command-specific help")
86
102
 
87
103
  for cmd_name, help_text in router.help_info.items():
88
-
89
104
  cmd_parser = subparsers.add_parser(cmd_name, help=help_text, add_help=False)
90
105
  cmd_parser.add_argument('command_args', nargs=argparse.REMAINDER,
91
106
  help='Arguments passed directly to the command handler')
92
107
 
93
108
  # Re-parse with command subparsers
94
- args = parser.parse_args([command_name] + all_args)
109
+ args = parser.parse_args([command_name.lstrip('/')] + all_args)
95
110
  command_args = args.command_args if hasattr(args, 'command_args') else []
96
111
  unknown_args = []
97
112
  else:
@@ -104,67 +119,86 @@ def main():
104
119
  args.model = global_model
105
120
  if args.provider is None:
106
121
  args.provider = global_provider
107
- # --- END OF FIX ---
108
- npc_instance = load_npc_by_name(args.npc, NPCSH_DB_PATH)
109
-
110
- effective_model = args.model or NPCSH_CHAT_MODEL
111
- effective_provider = args.provider or NPCSH_CHAT_PROVIDER
112
-
113
122
 
123
+ # Use npcsh's setup_shell to get proper team and NPC setup
124
+ try:
125
+ command_history, team, forenpc_obj = setup_shell()
126
+ except Exception as e:
127
+ print(f"Warning: Could not set up full npcsh environment: {e}", file=sys.stderr)
128
+ print("Falling back to basic NPC loading...", file=sys.stderr)
129
+ team = None
130
+ forenpc_obj = load_npc_by_name(args.npc, NPCSH_DB_PATH)
131
+
132
+ # Determine which NPC to use
133
+ npc_instance = None
134
+ if team and args.npc in team.npcs:
135
+ npc_instance = team.npcs[args.npc]
136
+ elif team and args.npc == team.forenpc.name if team.forenpc else False:
137
+ npc_instance = team.forenpc
138
+ else:
139
+ npc_instance = load_npc_by_name(args.npc, NPCSH_DB_PATH)
114
140
 
115
- extras = {}
141
+ if not npc_instance:
142
+ print(f"Error: Could not load NPC '{args.npc}'", file=sys.stderr)
143
+ sys.exit(1)
116
144
 
117
- # Process command args if we have a valid command
118
- if is_valid_command:
119
- # Parse command args properly
120
- if command_args:
121
- i = 0
122
- while i < len(command_args):
123
- arg = command_args[i]
124
- if arg.startswith("--"):
125
- param = arg[2:] # Remove --
126
- if "=" in param:
127
- param_name, param_value = param.split("=", 1)
128
- extras[param_name] = param_value
129
- i += 1
130
- elif i + 1 < len(command_args) and not command_args[i+1].startswith("--"):
131
- extras[param] = command_args[i+1]
132
- i += 2
133
- else:
134
- extras[param] = True
135
- i += 1
136
- else:
137
- i += 1
138
-
139
- handler = router.get_route(command_name)
140
- if not handler:
141
- print(f"Error: Command '{command_name}' recognized but no handler found.", file=sys.stderr)
142
- sys.exit(1)
143
-
144
- full_command_str = command_name
145
- if command_args:
146
- full_command_str += " " + " ".join(command_args)
145
+ # Now check for jinxs if we haven't identified a command yet
146
+ if not is_valid_command and all_args:
147
+ first_arg = all_args[0]
147
148
 
148
- handler_kwargs = {
149
- "model": effective_model,
150
- "provider": effective_provider,
151
- "npc": npc_instance,
152
- "api_url": NPCSH_API_URL,
153
- "stream": NPCSH_STREAM_OUTPUT,
154
- "messages": [],
155
- "team": None,
156
- "current_path": os.getcwd(),
157
- **extras
158
- }
159
-
160
- try:
161
- result = handler(command=full_command_str, **handler_kwargs)
149
+ # Check if first argument is a jinx name
150
+ jinx_found = False
151
+ if team and first_arg in team.jinxs_dict:
152
+ jinx_found = True
153
+ elif isinstance(npc_instance, NPC) and hasattr(npc_instance, 'jinxs_dict') and first_arg in npc_instance.jinxs_dict:
154
+ jinx_found = True
155
+
156
+ if jinx_found:
157
+ is_valid_command = True
158
+ command_name = '/' + first_arg
159
+ all_args = all_args[1:]
160
+
161
+ # Create a shell state object similar to npcsh
162
+ shell_state = initial_state
163
+ shell_state.npc = npc_instance
164
+ shell_state.team = team
165
+ shell_state.current_path = os.getcwd()
166
+ shell_state.stream_output = NPCSH_STREAM_OUTPUT
167
+
168
+ # Override model/provider if specified
169
+ effective_model = args.model or (npc_instance.model if npc_instance.model else NPCSH_CHAT_MODEL)
170
+ effective_provider = args.provider or (npc_instance.provider if npc_instance.provider else NPCSH_CHAT_PROVIDER)
171
+
172
+ # Update the NPC's model/provider for this session if overridden
173
+ if args.model:
174
+ npc_instance.model = effective_model
175
+ if args.provider:
176
+ npc_instance.provider = effective_provider
177
+ try:
178
+ if is_valid_command:
179
+ # Handle slash command using npcsh's execute_slash_command
180
+ full_command_str = command_name
181
+ if command_args:
182
+ full_command_str += " " + " ".join(command_args)
183
+
184
+ print(f"Executing command: {full_command_str}")
185
+
186
+ updated_state, result = execute_slash_command(
187
+ full_command_str,
188
+ stdin_input=None,
189
+ state=shell_state,
190
+ stream=NPCSH_STREAM_OUTPUT,
191
+ router = router
192
+ )
162
193
 
194
+ # Process and display the result
163
195
  if isinstance(result, dict):
164
196
  output = result.get("output") or result.get("response")
197
+ model_for_stream = result.get('model', effective_model)
198
+ provider_for_stream = result.get('provider', effective_provider)
165
199
 
166
200
  if NPCSH_STREAM_OUTPUT and not isinstance(output, str):
167
- print_and_process_stream_with_markdown(output, effective_model, effective_provider)
201
+ print_and_process_stream_with_markdown(output, model_for_stream, provider_for_stream)
168
202
  elif output is not None:
169
203
  render_markdown(str(output))
170
204
  elif result is not None:
@@ -172,45 +206,38 @@ def main():
172
206
  else:
173
207
  print(f"Command '{command_name}' executed.")
174
208
 
175
- except Exception as e:
176
- print(f"Error executing command '{command_name}': {e}", file=sys.stderr)
177
- traceback.print_exc()
178
- sys.exit(1)
179
- else:
180
- # Process as a prompt
181
- prompt = " ".join(unknown_args)
209
+ else:
210
+ # Process as a regular prompt using npcsh's execution logic
211
+ prompt = " ".join(unknown_args)
182
212
 
183
- if not prompt:
184
- # If no prompt and no command, show help
185
- parser.print_help()
186
- sys.exit(1)
213
+ if not prompt:
214
+ # If no prompt and no command, show help
215
+ parser.print_help()
216
+ sys.exit(1)
187
217
 
188
- print(f"Processing prompt: '{prompt}' with NPC: '{args.npc}'...")
189
- try:
190
- response_data = check_llm_command(
191
- command=prompt,
192
- model=effective_model,
193
- provider=effective_provider,
194
- npc=npc_instance,
195
- stream=NPCSH_STREAM_OUTPUT,
196
- messages=[],
197
- team=None,
198
- api_url=NPCSH_API_URL,
199
- )
218
+ print(f"Processing prompt: '{prompt}' with NPC: '{args.npc}'...")
219
+
220
+ # Use npcsh's execute_command but force it to chat mode for simple prompts
221
+ shell_state.current_mode = 'chat'
222
+ updated_state, result = execute_command(prompt, shell_state)
200
223
 
201
- if isinstance(response_data, dict):
202
- output = response_data.get("output")
224
+ # Process and display the result
225
+ if isinstance(result, dict):
226
+ output = result.get("output")
227
+ model_for_stream = result.get('model', effective_model)
228
+ provider_for_stream = result.get('provider', effective_provider)
229
+
203
230
  if NPCSH_STREAM_OUTPUT and hasattr(output, '__iter__') and not isinstance(output, (str, bytes, dict, list)):
204
- print_and_process_stream_with_markdown(output, effective_model, effective_provider)
231
+ print_and_process_stream_with_markdown(output, model_for_stream, provider_for_stream)
205
232
  elif output is not None:
206
233
  render_markdown(str(output))
207
- elif response_data is not None:
208
- render_markdown(str(response_data))
234
+ elif result is not None:
235
+ render_markdown(str(result))
209
236
 
210
- except Exception as e:
211
- print(f"Error processing prompt: {e}", file=sys.stderr)
212
- traceback.print_exc()
213
- sys.exit(1)
237
+ except Exception as e:
238
+ print(f"Error executing command: {e}", file=sys.stderr)
239
+ traceback.print_exc()
240
+ sys.exit(1)
214
241
 
215
242
  if __name__ == "__main__":
216
243
  main()