npcsh 1.0.8__py3-none-any.whl → 1.0.10__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 CHANGED
@@ -12,6 +12,14 @@ from termcolor import colored
12
12
 
13
13
 
14
14
  from typing import Dict, List
15
+ import subprocess
16
+ import termios
17
+ import tty
18
+ import pty
19
+ import select
20
+ import signal
21
+ import time
22
+ import os
15
23
  import re
16
24
  import sqlite3
17
25
  from datetime import datetime
@@ -436,6 +444,7 @@ def start_interactive_session(command: list) -> int:
436
444
  """
437
445
  Starts an interactive session. Only works on Unix. On Windows, print a message and return 1.
438
446
  """
447
+ ON_WINDOWS = platform.system().lower().startswith("win")
439
448
  if ON_WINDOWS or termios is None or tty is None or pty is None or select is None or signal is None:
440
449
  print("Interactive terminal sessions are not supported on Windows.")
441
450
  return 1
@@ -681,10 +690,21 @@ def validate_bash_command(command_parts: list) -> bool:
681
690
 
682
691
  if base_command == 'which':
683
692
  return False # disable which arbitrarily cause the command parsing for it is too finnicky.
693
+
694
+
695
+ # Allow interactive commands (ipython, python, sqlite3, r) as valid commands
696
+ INTERACTIVE_COMMANDS = ["ipython", "python", "sqlite3", "r"]
697
+ TERMINAL_EDITORS = ["vim", "nano", "emacs"]
698
+ if base_command in TERMINAL_EDITORS or base_command in INTERACTIVE_COMMANDS:
699
+ return True
700
+
684
701
  if base_command not in COMMAND_PATTERNS and base_command not in BASH_COMMANDS:
685
- return False # Allow other commands to pass through
702
+ return False # Not a recognized command
703
+
704
+ pattern = COMMAND_PATTERNS.get(base_command)
705
+ if not pattern:
706
+ return True # Allow commands in BASH_COMMANDS but not in COMMAND_PATTERNS
686
707
 
687
- pattern = COMMAND_PATTERNS[base_command]
688
708
  args = []
689
709
  flags = []
690
710
 
npcsh/alicanto.py CHANGED
@@ -14,11 +14,11 @@ import subprocess
14
14
  import networkx as nx
15
15
 
16
16
  from npcpy.npc_compiler import NPC
17
- from npcpy.llm_funcs import get_llm_response
17
+ from npcpy.llm_funcs import get_llm_response, extract_facts, identify_groups, assign_groups_to_fact
18
18
  from npcsh._state import NPCSH_CHAT_MODEL, NPCSH_CHAT_PROVIDER
19
19
  from npcpy.npc_sysenv import print_and_process_stream_with_markdown
20
- from npcpy.memory.deep_research import consolidate_research
21
- from npcpy.memory.knowledge_graph import extract_facts, identify_groups, assign_groups_to_fact
20
+
21
+
22
22
 
23
23
  def generate_random_npcs(num_npcs: int, model: str, provider: str, request: str) -> List[NPC]:
24
24
  """
npcsh/npc.py CHANGED
@@ -85,8 +85,6 @@ def main():
85
85
  help="Run 'npc <command> --help' for command-specific help")
86
86
 
87
87
  for cmd_name, help_text in router.help_info.items():
88
- if router.shell_only.get(cmd_name, False):
89
- continue
90
88
 
91
89
  cmd_parser = subparsers.add_parser(cmd_name, help=help_text, add_help=False)
92
90
  cmd_parser.add_argument('command_args', nargs=argparse.REMAINDER,
npcsh/npcsh.py CHANGED
@@ -31,9 +31,11 @@ import yaml
31
31
  # Local Application Imports
32
32
  from npcsh._state import (
33
33
  setup_npcsh_config,
34
+ initial_state,
34
35
  is_npcsh_initialized,
35
36
  initialize_base_npcs_if_needed,
36
37
  orange,
38
+ ShellState,
37
39
  interactive_commands,
38
40
  BASH_COMMANDS,
39
41
  start_interactive_session,
@@ -52,8 +54,6 @@ from npcpy.memory.command_history import (
52
54
  CommandHistory,
53
55
  save_conversation_message,
54
56
  )
55
- from npcpy.memory.knowledge_graph import breathe
56
- from npcpy.memory.sleep import sleep, forget
57
57
  from npcpy.npc_compiler import NPC, Team, load_jinxs_from_directory
58
58
  from npcpy.llm_funcs import check_llm_command, get_llm_response, execute_llm_command
59
59
  from npcpy.gen.embeddings import get_embeddings
@@ -83,7 +83,178 @@ except Exception as e:
83
83
 
84
84
 
85
85
 
86
- from npcsh._state import initial_state, ShellState
86
+
87
+ def get_path_executables() -> List[str]:
88
+ """Get executables from PATH (cached for performance)"""
89
+ if not hasattr(get_path_executables, '_cache'):
90
+ executables = set()
91
+ path_dirs = os.environ.get('PATH', '').split(os.pathsep)
92
+ for path_dir in path_dirs:
93
+ if os.path.isdir(path_dir):
94
+ try:
95
+ for item in os.listdir(path_dir):
96
+ item_path = os.path.join(path_dir, item)
97
+ if os.path.isfile(item_path) and os.access(item_path, os.X_OK):
98
+ executables.add(item)
99
+ except (PermissionError, OSError):
100
+ continue
101
+ get_path_executables._cache = sorted(list(executables))
102
+ return get_path_executables._cache
103
+
104
+
105
+ import logging
106
+
107
+ # Set up completion logger
108
+ completion_logger = logging.getLogger('npcsh.completion')
109
+ completion_logger.setLevel(logging.WARNING) # Default to WARNING (quiet)
110
+
111
+ # Add handler if not already present
112
+ if not completion_logger.handlers:
113
+ handler = logging.StreamHandler(sys.stderr)
114
+ formatter = logging.Formatter('[%(name)s] %(message)s')
115
+ handler.setFormatter(formatter)
116
+ completion_logger.addHandler(handler)
117
+
118
+ def make_completer(shell_state: ShellState):
119
+ def complete(text: str, state_index: int) -> Optional[str]:
120
+ """Main completion function"""
121
+ try:
122
+ buffer = readline.get_line_buffer()
123
+ begidx = readline.get_begidx()
124
+ endidx = readline.get_endidx()
125
+
126
+ completion_logger.debug(f"text='{text}', buffer='{buffer}', begidx={begidx}, endidx={endidx}, state_index={state_index}")
127
+
128
+ matches = []
129
+
130
+ # Check if we're completing a slash command
131
+ if begidx > 0 and buffer[begidx-1] == '/':
132
+ completion_logger.debug(f"Slash command completion - text='{text}'")
133
+ slash_commands = get_slash_commands(shell_state)
134
+ completion_logger.debug(f"Available slash commands: {slash_commands}")
135
+
136
+ if text == '':
137
+ matches = [cmd[1:] for cmd in slash_commands]
138
+ else:
139
+ full_text = '/' + text
140
+ matching_commands = [cmd for cmd in slash_commands if cmd.startswith(full_text)]
141
+ matches = [cmd[1:] for cmd in matching_commands]
142
+
143
+ completion_logger.debug(f"Slash command matches: {matches}")
144
+
145
+ elif is_command_position(buffer, begidx):
146
+ completion_logger.debug("Command position detected")
147
+ bash_matches = [cmd for cmd in BASH_COMMANDS if cmd.startswith(text)]
148
+ matches.extend(bash_matches)
149
+
150
+ interactive_matches = [cmd for cmd in interactive_commands.keys() if cmd.startswith(text)]
151
+ matches.extend(interactive_matches)
152
+
153
+ if len(text) >= 1:
154
+ path_executables = get_path_executables()
155
+ exec_matches = [cmd for cmd in path_executables if cmd.startswith(text)]
156
+ matches.extend(exec_matches[:20])
157
+ else:
158
+ completion_logger.debug("File completion")
159
+ matches = get_file_completions(text)
160
+
161
+ matches = sorted(list(set(matches)))
162
+ completion_logger.debug(f"Final matches: {matches}")
163
+
164
+ if state_index < len(matches):
165
+ result = matches[state_index]
166
+ completion_logger.debug(f"Returning: '{result}'")
167
+ return result
168
+ else:
169
+ completion_logger.debug(f"No match for state_index {state_index}")
170
+
171
+ except Exception as e:
172
+ completion_logger.error(f"Exception in completion: {e}")
173
+ completion_logger.debug("Exception details:", exc_info=True)
174
+
175
+ return None
176
+
177
+ return complete
178
+
179
+ def get_slash_commands(state: ShellState) -> List[str]:
180
+ """Get available slash commands from router and team"""
181
+ commands = []
182
+
183
+ completion_logger.debug("Getting slash commands...")
184
+
185
+ # Router commands
186
+ if router and hasattr(router, 'routes'):
187
+ router_cmds = [f"/{cmd}" for cmd in router.routes.keys()]
188
+ commands.extend(router_cmds)
189
+ completion_logger.debug(f"Router commands: {router_cmds}")
190
+
191
+ # Team jinxs
192
+ if state.team and hasattr(state.team, 'jinxs_dict'):
193
+ jinx_cmds = [f"/{jinx}" for jinx in state.team.jinxs_dict.keys()]
194
+ commands.extend(jinx_cmds)
195
+ completion_logger.debug(f"Jinx commands: {jinx_cmds}")
196
+
197
+ # NPC names for switching
198
+ if state.team and hasattr(state.team, 'npcs'):
199
+ npc_cmds = [f"/{npc}" for npc in state.team.npcs.keys()]
200
+ commands.extend(npc_cmds)
201
+ completion_logger.debug(f"NPC commands: {npc_cmds}")
202
+
203
+ # Mode switching commands
204
+ mode_cmds = ['/cmd', '/agent', '/chat', '/ride']
205
+ commands.extend(mode_cmds)
206
+ completion_logger.debug(f"Mode commands: {mode_cmds}")
207
+
208
+ result = sorted(commands)
209
+ completion_logger.debug(f"Final slash commands: {result}")
210
+ return result
211
+ def get_file_completions(text: str) -> List[str]:
212
+ """Get file/directory completions"""
213
+ try:
214
+ if text.startswith('/'):
215
+ basedir = os.path.dirname(text) or '/'
216
+ prefix = os.path.basename(text)
217
+ elif text.startswith('./') or text.startswith('../'):
218
+ basedir = os.path.dirname(text) or '.'
219
+ prefix = os.path.basename(text)
220
+ else:
221
+ basedir = '.'
222
+ prefix = text
223
+
224
+ if not os.path.exists(basedir):
225
+ return []
226
+
227
+ matches = []
228
+ try:
229
+ for item in os.listdir(basedir):
230
+ if item.startswith(prefix):
231
+ full_path = os.path.join(basedir, item)
232
+ if basedir == '.':
233
+ completion = item
234
+ else:
235
+ completion = os.path.join(basedir, item)
236
+
237
+ # Just return the name, let readline handle spacing/slashes
238
+ matches.append(completion)
239
+ except (PermissionError, OSError):
240
+ pass
241
+
242
+ return sorted(matches)
243
+ except Exception:
244
+ return []
245
+ def is_command_position(buffer: str, begidx: int) -> bool:
246
+ """Determine if cursor is at a command position"""
247
+ # Get the part of buffer before the current word
248
+ before_word = buffer[:begidx]
249
+
250
+ # Split by command separators
251
+ parts = re.split(r'[|;&]', before_word)
252
+ current_command_part = parts[-1].strip()
253
+
254
+ # If there's nothing before the current word in this command part,
255
+ # or only whitespace, we're at command position
256
+ return len(current_command_part) == 0
257
+
87
258
 
88
259
  def readline_safe_prompt(prompt: str) -> str:
89
260
  ansi_escape = re.compile(r"(\033\[[0-9;]*[a-zA-Z])")
@@ -233,29 +404,27 @@ def wrap_text(text: str, width: int = 80) -> str:
233
404
  # --- Readline Setup and Completion ---
234
405
 
235
406
  def setup_readline() -> str:
407
+ """Setup readline with history and completion"""
236
408
  try:
237
409
  readline.read_history_file(READLINE_HISTORY_FILE)
238
-
239
410
  readline.set_history_length(1000)
411
+
412
+ # Don't set completer here - it will be set in run_repl with state
413
+ readline.parse_and_bind("tab: complete")
414
+
240
415
  readline.parse_and_bind("set enable-bracketed-paste on")
241
- #readline.parse_and_bind('"\e[A": history-search-backward')
242
- #readline.parse_and_bind('"\e[B": history-search-forward')
243
416
  readline.parse_and_bind(r'"\C-r": reverse-search-history')
244
417
  readline.parse_and_bind(r'"\C-e": end-of-line')
245
418
  readline.parse_and_bind(r'"\C-a": beginning-of-line')
246
- #if sys.platform == "darwin":
247
- # readline.parse_and_bind("bind ^I rl_complete")
248
- #else:
249
- # readline.parse_and_bind("tab: complete")
250
-
419
+
251
420
  return READLINE_HISTORY_FILE
252
-
253
-
421
+
254
422
  except FileNotFoundError:
255
423
  pass
256
424
  except OSError as e:
257
425
  print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
258
426
 
427
+
259
428
  def save_readline_history():
260
429
  try:
261
430
  readline.write_history_file(READLINE_HISTORY_FILE)
@@ -263,52 +432,11 @@ def save_readline_history():
263
432
  print(f"Warning: Could not write readline history file {READLINE_HISTORY_FILE}: {e}")
264
433
 
265
434
 
266
- # --- Placeholder for actual valid commands ---
267
- # This should be populated dynamically based on router, builtins, and maybe PATH executables
268
- valid_commands_list = list(router.routes.keys()) + list(interactive_commands.keys()) + ["cd", "exit", "quit"] + BASH_COMMANDS
269
435
 
270
- def complete(text: str, state: int) -> Optional[str]:
271
- try:
272
- buffer = readline.get_line_buffer()
273
- except:
274
- print('couldnt get readline buffer')
275
- line_parts = parse_command_safely(buffer) # Use safer parsing
276
- word_before_cursor = ""
277
- if len(line_parts) > 0 and not buffer.endswith(' '):
278
- current_word = line_parts[-1]
279
- else:
280
- current_word = "" # Completing after a space
281
436
 
282
- try:
283
- # Command completion (start of line or after pipe/semicolon)
284
- # This needs refinement to detect context better
285
- is_command_start = not line_parts or (len(line_parts) == 1 and not buffer.endswith(' ')) # Basic check
286
- if is_command_start and not text.startswith('-'): # Don't complete options as commands
287
- cmd_matches = [cmd + ' ' for cmd in valid_commands_list if cmd.startswith(text)]
288
- # Add executables from PATH? (Can be slow)
289
- # path_executables = [f + ' ' for f in shutil.get_exec_path() if os.path.basename(f).startswith(text)]
290
- # cmd_matches.extend(path_executables)
291
- return cmd_matches[state]
292
-
293
- # File/Directory completion (basic)
294
- # Improve context awareness (e.g., after 'cd', 'ls', 'cat', etc.)
295
- if text and (not text.startswith('/') or os.path.exists(os.path.dirname(text))):
296
- basedir = os.path.dirname(text)
297
- prefix = os.path.basename(text)
298
- search_dir = basedir if basedir else '.'
299
- try:
300
- matches = [os.path.join(basedir, f) + ('/' if os.path.isdir(os.path.join(search_dir, f)) else ' ')
301
- for f in os.listdir(search_dir) if f.startswith(prefix)]
302
- return matches[state]
303
- except OSError: # Handle permission denied etc.
304
- return None
305
-
306
- except IndexError:
307
- return None
308
- except Exception: # Catch broad exceptions during completion
309
- return None
437
+ valid_commands_list = list(router.routes.keys()) + list(interactive_commands.keys()) + ["cd", "exit", "quit"] + BASH_COMMANDS
438
+
310
439
 
311
- return None
312
440
 
313
441
 
314
442
  # --- Command Execution Logic ---
@@ -455,10 +583,8 @@ def execute_slash_command(command: str, stdin_input: Optional[str], state: Shell
455
583
  result_dict = handler(command, **handler_kwargs)
456
584
 
457
585
  if isinstance(result_dict, dict):
458
- #some respond with output, some with response, needs to be fixed upstream
459
- output = result_dict.get("output") or result_dict.get("response")
460
586
  state.messages = result_dict.get("messages", state.messages)
461
- return state, output
587
+ return state, result_dict
462
588
  else:
463
589
  return state, result_dict
464
590
 
@@ -572,7 +698,7 @@ def process_pipeline_command(
572
698
  images=state.attachments,
573
699
  stream=stream_final,
574
700
  context=info,
575
- shell=True,
701
+
576
702
  )
577
703
  if isinstance(llm_result, dict):
578
704
  state.messages = llm_result.get("messages", state.messages)
@@ -729,8 +855,6 @@ def execute_command(
729
855
  try:
730
856
  bash_state, bash_output = handle_bash_command(cmd_parts, command, None, state)
731
857
  return bash_state, bash_output
732
- except CommandNotFoundError:
733
- return state, colored(f"Command not found: {command_name}", "red")
734
858
  except Exception as bash_err:
735
859
  return state, colored(f"Bash execution failed: {bash_err}", "red")
736
860
  except Exception:
@@ -1007,12 +1131,13 @@ def execute_todo_item(todo: Dict[str, Any], ride_state: RideState, shell_state:
1007
1131
  team=shell_state.team,
1008
1132
  messages=[],
1009
1133
  stream=shell_state.stream_output,
1010
- shell=True,
1134
+
1011
1135
  )
1012
1136
 
1013
1137
  output_payload = result.get("output", "")
1014
1138
  output_str = ""
1015
1139
 
1140
+
1016
1141
  if isgenerator(output_payload):
1017
1142
  output_str = print_and_process_stream_with_markdown(output_payload, shell_state.chat_model, shell_state.chat_provider)
1018
1143
  elif isinstance(output_payload, dict):
@@ -1146,7 +1271,6 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
1146
1271
  command_history = CommandHistory(db_path)
1147
1272
 
1148
1273
  try:
1149
- readline.set_completer(complete)
1150
1274
  history_file = setup_readline()
1151
1275
  atexit.register(save_readline_history)
1152
1276
  atexit.register(command_history.close)
@@ -1269,34 +1393,30 @@ def process_result(
1269
1393
  if user_input =='/help':
1270
1394
  render_markdown(output)
1271
1395
  elif result_state.stream_output:
1272
-
1273
- try:
1274
- final_output_str = print_and_process_stream_with_markdown(output, result_state.chat_model, result_state.chat_provider)
1275
- except AttributeError as e:
1276
- if isinstance(output, str):
1277
- if len(output) > 0:
1278
- final_output_str = output
1279
- render_markdown(final_output_str)
1280
- except TypeError as e:
1281
-
1282
- if isinstance(output, str):
1283
- if len(output) > 0:
1284
- final_output_str = output
1285
- render_markdown(final_output_str)
1286
- elif isinstance(output, dict):
1287
- if 'output' in output:
1288
- final_output_str = output['output']
1289
- render_markdown(final_output_str)
1396
+
1397
+ if isinstance(output, dict):
1398
+ output_gen = output.get('output')
1399
+ model = output.get('model', result_state.chat_model)
1400
+ provider = output.get('provider', result_state.chat_provider)
1401
+ else:
1402
+ output_gen = output
1403
+ model = result_state.chat_model
1404
+ provider = result_state.chat_provider
1405
+ print('processing stream output with markdown...')
1406
+
1407
+ final_output_str = print_and_process_stream_with_markdown(output_gen,
1408
+ model,
1409
+ provider)
1290
1410
 
1291
1411
  elif output is not None:
1292
1412
  final_output_str = str(output)
1293
- render_markdown(final_output_str)
1413
+ render_markdown('str not none: ', final_output_str)
1294
1414
  if final_output_str and result_state.messages and result_state.messages[-1].get("role") != "assistant":
1295
1415
  result_state.messages.append({"role": "assistant", "content": final_output_str})
1296
1416
 
1297
1417
  #print(result_state.messages)
1298
1418
 
1299
- print() # Add spacing after output
1419
+
1300
1420
 
1301
1421
  if final_output_str:
1302
1422
  save_conversation_message(
@@ -1317,6 +1437,11 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1317
1437
  print(f'Using {state.current_mode} mode. Use /agent, /cmd, /chat, or /ride to switch to other modes')
1318
1438
  print(f'To switch to a different NPC, type /<npc_name>')
1319
1439
  is_windows = platform.system().lower().startswith("win")
1440
+ try:
1441
+ completer = make_completer(state)
1442
+ readline.set_completer(completer)
1443
+ except:
1444
+ pass
1320
1445
 
1321
1446
  def exit_shell(state):
1322
1447
  print("\nGoodbye!")
@@ -1336,6 +1461,12 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1336
1461
 
1337
1462
  while True:
1338
1463
  try:
1464
+ try:
1465
+ completer = make_completer(state)
1466
+ readline.set_completer(completer)
1467
+ except:
1468
+ pass
1469
+
1339
1470
  if is_windows:
1340
1471
  cwd_part = os.path.basename(state.current_path)
1341
1472
  if isinstance(state.npc, NPC):
@@ -1350,12 +1481,6 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1350
1481
  else:
1351
1482
  prompt_end = f":🤖{colored('npc', 'blue', attrs=['bold'])}{colored('sh', 'yellow')}> "
1352
1483
  prompt = readline_safe_prompt(f"{cwd_colored}{prompt_end}")
1353
- cwd_colored = colored(os.path.basename(state.current_path), "blue")
1354
- if isinstance(state.npc, NPC):
1355
- prompt_end = f":🤖{orange(state.npc.name)}> "
1356
- else:
1357
- prompt_end = f":🤖{colored('npc', 'blue', attrs=['bold'])}{colored('sh', 'yellow')}> "
1358
- prompt = readline_safe_prompt(f"{cwd_colored}{prompt_end}")
1359
1484
 
1360
1485
  user_input = get_multiline_input(prompt).strip()
1361
1486
  # Handle Ctrl+Z (ASCII SUB, '\x1a') as exit (Windows and Unix)
npcsh/routes.py CHANGED
@@ -24,6 +24,7 @@ from npcpy.llm_funcs import (
24
24
  get_llm_response,
25
25
  gen_image,
26
26
  gen_video,
27
+ breathe,
27
28
  )
28
29
  from npcpy.npc_compiler import NPC, Team, Jinx
29
30
  from npcpy.npc_compiler import initialize_npc_project
@@ -38,8 +39,8 @@ from npcpy.memory.search import execute_rag_command, execute_search_command, exe
38
39
  from npcpy.memory.command_history import CommandHistory
39
40
 
40
41
 
41
- from npcpy.memory.knowledge_graph import breathe
42
- from npcpy.memory.sleep import sleep, forget
42
+
43
+
43
44
  from npcpy.serve import start_flask_server
44
45
 
45
46
 
@@ -62,13 +63,11 @@ class CommandRouter:
62
63
  def __init__(self):
63
64
  self.routes = {}
64
65
  self.help_info = {}
65
- self.shell_only = {}
66
66
 
67
- def route(self, command: str, help_text: str = "", shell_only: bool = False) -> Callable:
67
+ def route(self, command: str, help_text: str = "") -> Callable:
68
68
  def wrapper(func):
69
69
  self.routes[command] = func
70
70
  self.help_info[command] = help_text
71
- self.shell_only[command] = shell_only
72
71
 
73
72
  @functools.wraps(func)
74
73
  def wrapped_func(*args, **kwargs):
@@ -102,13 +101,12 @@ router = CommandRouter()
102
101
  def get_help_text():
103
102
  commands = router.get_commands()
104
103
  help_info = router.help_info
105
- shell_only = router.shell_only
104
+
106
105
  commands.sort()
107
106
  output = "# Available Commands\n\n"
108
107
  for cmd in commands:
109
108
  help_text = help_info.get(cmd, "")
110
- shell_only_text = " (Shell only)" if shell_only.get(cmd, False) else ""
111
- output += f"/{cmd}{shell_only_text} - {help_text}\n\n"
109
+ output += f"/{cmd} - {help_text}\n\n"
112
110
  output += """
113
111
  # Note
114
112
  - Bash commands and programs can be executed directly (try bash first, then LLM).
@@ -120,7 +118,7 @@ def get_help_text():
120
118
  def safe_get(kwargs, key, default=None):
121
119
  return kwargs.get(key, default)
122
120
 
123
- @router.route("breathe", "Condense context on a regular cadence", shell_only=True)
121
+ @router.route("breathe", "Condense context on a regular cadence")
124
122
  def breathe_handler(command: str, **kwargs):
125
123
  messages = safe_get(kwargs, "messages", [])
126
124
  npc = safe_get(kwargs, "npc")
@@ -162,7 +160,7 @@ def compile_handler(command: str, **kwargs):
162
160
 
163
161
 
164
162
 
165
- @router.route("flush", "Flush the last N messages", shell_only=True)
163
+ @router.route("flush", "Flush the last N messages")
166
164
  def flush_handler(command: str, **kwargs):
167
165
  messages = safe_get(kwargs, "messages", [])
168
166
  try:
@@ -260,6 +258,28 @@ def init_handler(command: str, **kwargs):
260
258
  traceback.print_exc()
261
259
  output = f"Error initializing project: {e}"
262
260
  return {"output": output, "messages": messages}
261
+ # Add these route handlers after the existing imports (around line 50):
262
+ @router.route("n")
263
+ @router.route("npc")
264
+ def switch_npc_handler(command: str, **kwargs) -> dict:
265
+ """Switch to a different NPC"""
266
+ team = kwargs.get('team')
267
+ parts = command.split()
268
+
269
+ if len(parts) < 2:
270
+ if team:
271
+ available_npcs = list(team.npcs.keys())
272
+ return {"output": f"Available NPCs: {', '.join(available_npcs)}"}
273
+ return {"output": "No team loaded or no NPC specified"}
274
+
275
+ npc_name = parts[1]
276
+ if team and npc_name in team.npcs:
277
+ # We can't directly modify the state here, so return a special signal
278
+ return {"output": f"SWITCH_NPC:{npc_name}"}
279
+ else:
280
+ available_npcs = list(team.npcs.keys()) if team else []
281
+ return {"output": f"NPC '{npc_name}' not found. Available: {', '.join(available_npcs)}"}
282
+
263
283
 
264
284
 
265
285
  @router.route("ots", "Take screenshot and optionally analyze with vision model")
@@ -284,7 +304,7 @@ def ots_handler(command: str, **kwargs):
284
304
  else:
285
305
  return {"output": f"Error: Image file not found at {full_path}", "messages": messages}
286
306
  else:
287
- screenshot_info = capture_screenshot(npc=npc)
307
+ screenshot_info = capture_screenshot(full=False)
288
308
  if screenshot_info and "file_path" in screenshot_info:
289
309
  image_paths.append(screenshot_info["file_path"])
290
310
  print(f"Screenshot captured: {screenshot_info.get('filename', os.path.basename(screenshot_info['file_path']))}")
@@ -317,7 +337,7 @@ def ots_handler(command: str, **kwargs):
317
337
  api_url=safe_get(kwargs, 'api_url'),
318
338
  api_key=safe_get(kwargs, 'api_key')
319
339
  )
320
- return {"output": response_data.get('response'), "messages": response_data.get('messages')}
340
+ return {"output": response_data.get('response'), "messages": response_data.get('messages'), "model": vision_model, "provider": vision_provider}
321
341
 
322
342
  except Exception as e:
323
343
  traceback.print_exc()
@@ -823,7 +843,7 @@ def wander_handler(command: str, **kwargs):
823
843
  traceback.print_exc()
824
844
  return {"output": f"Error during wander mode: {e}", "messages": messages}
825
845
 
826
- @router.route("yap", "Enter voice chat (yap) mode", shell_only=True)
846
+ @router.route("yap", "Enter voice chat (yap) mode")
827
847
  def whisper_handler(command: str, **kwargs):
828
848
  try:
829
849
  return enter_yap_mode(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.8
3
+ Version: 1.0.10
4
4
  Summary: npcsh is a command-line toolkit for using AI agents in novel ways.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcsh
6
6
  Author: Christopher Agostino
@@ -0,0 +1,21 @@
1
+ npcsh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ npcsh/_state.py,sha256=GCMUIwgIBlS7LEBLYlfBiPNKVaK19ZyxT833NFU-djU,31109
3
+ npcsh/alicanto.py,sha256=F-zZGjBTo3a_PQHvPC8-DNF6t4mJELo_zx7gBGvDehg,44611
4
+ npcsh/guac.py,sha256=Ocmk_c4NUtGsC3JOtmkbgLvD6u-XtBPRFRYcckpgUJU,33099
5
+ npcsh/mcp_helpers.py,sha256=Ktd2yXuBnLL2P7OMalgGLj84PXJSzaucjqmJVvWx6HA,12723
6
+ npcsh/mcp_npcsh.py,sha256=SfmplH62GS9iI6q4vuQLVUS6tkrok6L7JxODx_iH7ps,36158
7
+ npcsh/mcp_server.py,sha256=l2Ra0lpFrUu334pvp0Q9ajF2n73KvZswFi0FgbDhh9k,5884
8
+ npcsh/npc.py,sha256=7ujKrMQFgkeGJ4sX5Kn_dB5tjrPN58xeC91PNt453aM,7827
9
+ npcsh/npcsh.py,sha256=xrDcmK3UlkTXF2tdR1Bxq7sqG62-AcUTf_IupJYt8U4,59237
10
+ npcsh/plonk.py,sha256=U2e9yUJZN95Girzzvgrh-40zOdl5zO3AHPsIjoyLv2M,15261
11
+ npcsh/pti.py,sha256=jGHGE5SeIcDkV8WlOEHCKQCnYAL4IPS-kUBHrUz0oDA,10019
12
+ npcsh/routes.py,sha256=JNlRMk8WvGlR950Bl_LQMjm_gtdpPI0XMBQlj_YjI5I,37768
13
+ npcsh/spool.py,sha256=GhnSFX9uAtrB4m_ijuyA5tufH12DrWdABw0z8FmiCHc,11497
14
+ npcsh/wander.py,sha256=BiN6eYyFnEsFzo8MFLRkdZ8xS9sTKkQpjiCcy9chMcc,23225
15
+ npcsh/yap.py,sha256=h5KNt9sNOrDPhGe_zfn_yFIeQhizX09zocjcPWH7m3k,20905
16
+ npcsh-1.0.10.dist-info/licenses/LICENSE,sha256=IKBvAECHP-aCiJtE4cHGCE5Yl0tozYz02PomGeWS3y4,1070
17
+ npcsh-1.0.10.dist-info/METADATA,sha256=kaQ9neGi6CVloTZFgFdudx_oQZ9MHhlEqnwBoUp47Os,22748
18
+ npcsh-1.0.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ npcsh-1.0.10.dist-info/entry_points.txt,sha256=qxOYTm3ym3JWyWf2nv2Mk71uMcJIdUoNEJ8VYMkyHiY,214
20
+ npcsh-1.0.10.dist-info/top_level.txt,sha256=kHSNgKMCkfjV95-DH0YSp1LLBi0HXdF3w57j7MQON3E,6
21
+ npcsh-1.0.10.dist-info/RECORD,,
@@ -1,21 +0,0 @@
1
- npcsh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- npcsh/_state.py,sha256=IsWS9uIOFv7X8vpcLjjErCaFoJFlrzqxTduAZ6qGpMs,30549
3
- npcsh/alicanto.py,sha256=zJF5YwSNvtbK2EUKXzG45WOCMsSFu5cek5jCR7FgiuE,44709
4
- npcsh/guac.py,sha256=Ocmk_c4NUtGsC3JOtmkbgLvD6u-XtBPRFRYcckpgUJU,33099
5
- npcsh/mcp_helpers.py,sha256=Ktd2yXuBnLL2P7OMalgGLj84PXJSzaucjqmJVvWx6HA,12723
6
- npcsh/mcp_npcsh.py,sha256=SfmplH62GS9iI6q4vuQLVUS6tkrok6L7JxODx_iH7ps,36158
7
- npcsh/mcp_server.py,sha256=l2Ra0lpFrUu334pvp0Q9ajF2n73KvZswFi0FgbDhh9k,5884
8
- npcsh/npc.py,sha256=nKgNg6BZsp40znH2wbSnd6td6pvOM82csn_6og0Bx58,7907
9
- npcsh/npcsh.py,sha256=yRpTvpy8OiVCyWpHfNGfuueojwy2ZHnoZii91S4fKaQ,55229
10
- npcsh/plonk.py,sha256=U2e9yUJZN95Girzzvgrh-40zOdl5zO3AHPsIjoyLv2M,15261
11
- npcsh/pti.py,sha256=jGHGE5SeIcDkV8WlOEHCKQCnYAL4IPS-kUBHrUz0oDA,10019
12
- npcsh/routes.py,sha256=ufQVc6aqgC14_YHV88iwV53TN1Pk095NB6gFDqQqfB4,37208
13
- npcsh/spool.py,sha256=GhnSFX9uAtrB4m_ijuyA5tufH12DrWdABw0z8FmiCHc,11497
14
- npcsh/wander.py,sha256=BiN6eYyFnEsFzo8MFLRkdZ8xS9sTKkQpjiCcy9chMcc,23225
15
- npcsh/yap.py,sha256=h5KNt9sNOrDPhGe_zfn_yFIeQhizX09zocjcPWH7m3k,20905
16
- npcsh-1.0.8.dist-info/licenses/LICENSE,sha256=IKBvAECHP-aCiJtE4cHGCE5Yl0tozYz02PomGeWS3y4,1070
17
- npcsh-1.0.8.dist-info/METADATA,sha256=dyfZu_kNq1aT4XAKz319ILVW12U8NLNBog4BG_o2ias,22747
18
- npcsh-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- npcsh-1.0.8.dist-info/entry_points.txt,sha256=qxOYTm3ym3JWyWf2nv2Mk71uMcJIdUoNEJ8VYMkyHiY,214
20
- npcsh-1.0.8.dist-info/top_level.txt,sha256=kHSNgKMCkfjV95-DH0YSp1LLBi0HXdF3w57j7MQON3E,6
21
- npcsh-1.0.8.dist-info/RECORD,,
File without changes