npcsh 1.0.7__tar.gz → 1.0.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.7
3
+ Version: 1.0.9
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
@@ -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
@@ -552,6 +561,128 @@ def validate_bash_command(command_parts: list) -> bool:
552
561
  "flags": ["-a", "-e", "-t", "-f", "-F", "-W", "-n", "-g", "-h"],
553
562
  "requires_arg": True,
554
563
  },
564
+ "ls": {
565
+ "flags": [
566
+ "-a",
567
+ "-l",
568
+ "-h",
569
+ "-R",
570
+ "-t",
571
+ "-S",
572
+ "-r",
573
+ "-d",
574
+ "-F",
575
+ "-i",
576
+ "--color",
577
+ ],
578
+ "requires_arg": False,
579
+ },
580
+ "cp": {
581
+ "flags": [
582
+ "-r",
583
+ "-f",
584
+ "-i",
585
+ "-u",
586
+ "-v",
587
+ "--preserve",
588
+ "--no-preserve=mode,ownership,timestamps",
589
+ ],
590
+ "requires_arg": True,
591
+ },
592
+ "mv": {
593
+ "flags": ["-f", "-i", "-u", "-v", "--backup", "--no-clobber"],
594
+ "requires_arg": True,
595
+ },
596
+ "rm": {
597
+ "flags": ["-f", "-i", "-r", "-v", "--preserve-root", "--no-preserve-root"],
598
+ "requires_arg": True,
599
+ },
600
+ "mkdir": {
601
+ "flags": ["-p", "-v", "-m", "--mode", "--parents"],
602
+ "requires_arg": True,
603
+ },
604
+ "rmdir": {
605
+ "flags": ["-p", "-v", "--ignore-fail-on-non-empty"],
606
+ "requires_arg": True,
607
+ },
608
+ "touch": {
609
+ "flags": ["-a", "-c", "-m", "-r", "-d", "--date"],
610
+ "requires_arg": True,
611
+ },
612
+ "grep": {
613
+ "flags": [
614
+ "-i",
615
+ "-v",
616
+ "-r",
617
+ "-l",
618
+ "-n",
619
+ "-c",
620
+ "-w",
621
+ "-x",
622
+ "--color",
623
+ "--exclude",
624
+ "--include",
625
+ ],
626
+ "requires_arg": True,
627
+ },
628
+ "sed": {
629
+ "flags": [
630
+ "-e",
631
+ "-f",
632
+ "-i",
633
+ "-n",
634
+ "--expression",
635
+ "--file",
636
+ "--in-place",
637
+ "--quiet",
638
+ "--silent",
639
+ ],
640
+ "requires_arg": True,
641
+ },
642
+ "awk": {
643
+ "flags": [
644
+ "-f",
645
+ "-v",
646
+ "--file",
647
+ "--source",
648
+ "--assign",
649
+ "--posix",
650
+ "--traditional",
651
+ ],
652
+ "requires_arg": True,
653
+ },
654
+ "sort": {
655
+ "flags": [
656
+ "-b",
657
+ "-d",
658
+ "-f",
659
+ "-g",
660
+ "-i",
661
+ "-n",
662
+ "-r",
663
+ "-u",
664
+ "--check",
665
+ "--ignore-case",
666
+ "--numeric-sort",
667
+ ],
668
+ "requires_arg": False,
669
+ },
670
+ "uniq": {
671
+ "flags": ["-c", "-d", "-u", "-i", "--check-chars", "--skip-chars"],
672
+ "requires_arg": False,
673
+ },
674
+ "wc": {
675
+ "flags": ["-c", "-l", "-w", "-m", "-L", "--bytes", "--lines", "--words"],
676
+ "requires_arg": False,
677
+ },
678
+ "pwd": {
679
+ "flags": ["-L", "-P"],
680
+ "requires_arg": False,
681
+ },
682
+ "chmod": {
683
+ "flags": ["-R", "-v", "-c", "--reference"],
684
+ "requires_arg": True,
685
+ },
555
686
 
556
687
  }
557
688
 
@@ -559,10 +690,21 @@ def validate_bash_command(command_parts: list) -> bool:
559
690
 
560
691
  if base_command == 'which':
561
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
+
562
701
  if base_command not in COMMAND_PATTERNS and base_command not in BASH_COMMANDS:
563
- 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
564
707
 
565
- pattern = COMMAND_PATTERNS[base_command]
566
708
  args = []
567
709
  flags = []
568
710
 
@@ -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,
@@ -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,
@@ -83,7 +85,178 @@ except Exception as e:
83
85
 
84
86
 
85
87
 
86
- from npcsh._state import initial_state, ShellState
88
+
89
+ def get_path_executables() -> List[str]:
90
+ """Get executables from PATH (cached for performance)"""
91
+ if not hasattr(get_path_executables, '_cache'):
92
+ executables = set()
93
+ path_dirs = os.environ.get('PATH', '').split(os.pathsep)
94
+ for path_dir in path_dirs:
95
+ if os.path.isdir(path_dir):
96
+ try:
97
+ for item in os.listdir(path_dir):
98
+ item_path = os.path.join(path_dir, item)
99
+ if os.path.isfile(item_path) and os.access(item_path, os.X_OK):
100
+ executables.add(item)
101
+ except (PermissionError, OSError):
102
+ continue
103
+ get_path_executables._cache = sorted(list(executables))
104
+ return get_path_executables._cache
105
+
106
+
107
+ import logging
108
+
109
+ # Set up completion logger
110
+ completion_logger = logging.getLogger('npcsh.completion')
111
+ completion_logger.setLevel(logging.WARNING) # Default to WARNING (quiet)
112
+
113
+ # Add handler if not already present
114
+ if not completion_logger.handlers:
115
+ handler = logging.StreamHandler(sys.stderr)
116
+ formatter = logging.Formatter('[%(name)s] %(message)s')
117
+ handler.setFormatter(formatter)
118
+ completion_logger.addHandler(handler)
119
+
120
+ def make_completer(shell_state: ShellState):
121
+ def complete(text: str, state_index: int) -> Optional[str]:
122
+ """Main completion function"""
123
+ try:
124
+ buffer = readline.get_line_buffer()
125
+ begidx = readline.get_begidx()
126
+ endidx = readline.get_endidx()
127
+
128
+ completion_logger.debug(f"text='{text}', buffer='{buffer}', begidx={begidx}, endidx={endidx}, state_index={state_index}")
129
+
130
+ matches = []
131
+
132
+ # Check if we're completing a slash command
133
+ if begidx > 0 and buffer[begidx-1] == '/':
134
+ completion_logger.debug(f"Slash command completion - text='{text}'")
135
+ slash_commands = get_slash_commands(shell_state)
136
+ completion_logger.debug(f"Available slash commands: {slash_commands}")
137
+
138
+ if text == '':
139
+ matches = [cmd[1:] for cmd in slash_commands]
140
+ else:
141
+ full_text = '/' + text
142
+ matching_commands = [cmd for cmd in slash_commands if cmd.startswith(full_text)]
143
+ matches = [cmd[1:] for cmd in matching_commands]
144
+
145
+ completion_logger.debug(f"Slash command matches: {matches}")
146
+
147
+ elif is_command_position(buffer, begidx):
148
+ completion_logger.debug("Command position detected")
149
+ bash_matches = [cmd for cmd in BASH_COMMANDS if cmd.startswith(text)]
150
+ matches.extend(bash_matches)
151
+
152
+ interactive_matches = [cmd for cmd in interactive_commands.keys() if cmd.startswith(text)]
153
+ matches.extend(interactive_matches)
154
+
155
+ if len(text) >= 1:
156
+ path_executables = get_path_executables()
157
+ exec_matches = [cmd for cmd in path_executables if cmd.startswith(text)]
158
+ matches.extend(exec_matches[:20])
159
+ else:
160
+ completion_logger.debug("File completion")
161
+ matches = get_file_completions(text)
162
+
163
+ matches = sorted(list(set(matches)))
164
+ completion_logger.debug(f"Final matches: {matches}")
165
+
166
+ if state_index < len(matches):
167
+ result = matches[state_index]
168
+ completion_logger.debug(f"Returning: '{result}'")
169
+ return result
170
+ else:
171
+ completion_logger.debug(f"No match for state_index {state_index}")
172
+
173
+ except Exception as e:
174
+ completion_logger.error(f"Exception in completion: {e}")
175
+ completion_logger.debug("Exception details:", exc_info=True)
176
+
177
+ return None
178
+
179
+ return complete
180
+
181
+ def get_slash_commands(state: ShellState) -> List[str]:
182
+ """Get available slash commands from router and team"""
183
+ commands = []
184
+
185
+ completion_logger.debug("Getting slash commands...")
186
+
187
+ # Router commands
188
+ if router and hasattr(router, 'routes'):
189
+ router_cmds = [f"/{cmd}" for cmd in router.routes.keys()]
190
+ commands.extend(router_cmds)
191
+ completion_logger.debug(f"Router commands: {router_cmds}")
192
+
193
+ # Team jinxs
194
+ if state.team and hasattr(state.team, 'jinxs_dict'):
195
+ jinx_cmds = [f"/{jinx}" for jinx in state.team.jinxs_dict.keys()]
196
+ commands.extend(jinx_cmds)
197
+ completion_logger.debug(f"Jinx commands: {jinx_cmds}")
198
+
199
+ # NPC names for switching
200
+ if state.team and hasattr(state.team, 'npcs'):
201
+ npc_cmds = [f"/{npc}" for npc in state.team.npcs.keys()]
202
+ commands.extend(npc_cmds)
203
+ completion_logger.debug(f"NPC commands: {npc_cmds}")
204
+
205
+ # Mode switching commands
206
+ mode_cmds = ['/cmd', '/agent', '/chat', '/ride']
207
+ commands.extend(mode_cmds)
208
+ completion_logger.debug(f"Mode commands: {mode_cmds}")
209
+
210
+ result = sorted(commands)
211
+ completion_logger.debug(f"Final slash commands: {result}")
212
+ return result
213
+ def get_file_completions(text: str) -> List[str]:
214
+ """Get file/directory completions"""
215
+ try:
216
+ if text.startswith('/'):
217
+ basedir = os.path.dirname(text) or '/'
218
+ prefix = os.path.basename(text)
219
+ elif text.startswith('./') or text.startswith('../'):
220
+ basedir = os.path.dirname(text) or '.'
221
+ prefix = os.path.basename(text)
222
+ else:
223
+ basedir = '.'
224
+ prefix = text
225
+
226
+ if not os.path.exists(basedir):
227
+ return []
228
+
229
+ matches = []
230
+ try:
231
+ for item in os.listdir(basedir):
232
+ if item.startswith(prefix):
233
+ full_path = os.path.join(basedir, item)
234
+ if basedir == '.':
235
+ completion = item
236
+ else:
237
+ completion = os.path.join(basedir, item)
238
+
239
+ # Just return the name, let readline handle spacing/slashes
240
+ matches.append(completion)
241
+ except (PermissionError, OSError):
242
+ pass
243
+
244
+ return sorted(matches)
245
+ except Exception:
246
+ return []
247
+ def is_command_position(buffer: str, begidx: int) -> bool:
248
+ """Determine if cursor is at a command position"""
249
+ # Get the part of buffer before the current word
250
+ before_word = buffer[:begidx]
251
+
252
+ # Split by command separators
253
+ parts = re.split(r'[|;&]', before_word)
254
+ current_command_part = parts[-1].strip()
255
+
256
+ # If there's nothing before the current word in this command part,
257
+ # or only whitespace, we're at command position
258
+ return len(current_command_part) == 0
259
+
87
260
 
88
261
  def readline_safe_prompt(prompt: str) -> str:
89
262
  ansi_escape = re.compile(r"(\033\[[0-9;]*[a-zA-Z])")
@@ -233,29 +406,27 @@ def wrap_text(text: str, width: int = 80) -> str:
233
406
  # --- Readline Setup and Completion ---
234
407
 
235
408
  def setup_readline() -> str:
409
+ """Setup readline with history and completion"""
236
410
  try:
237
411
  readline.read_history_file(READLINE_HISTORY_FILE)
238
-
239
412
  readline.set_history_length(1000)
413
+
414
+ # Don't set completer here - it will be set in run_repl with state
415
+ readline.parse_and_bind("tab: complete")
416
+
240
417
  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
418
  readline.parse_and_bind(r'"\C-r": reverse-search-history')
244
419
  readline.parse_and_bind(r'"\C-e": end-of-line')
245
420
  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
-
421
+
251
422
  return READLINE_HISTORY_FILE
252
-
253
-
423
+
254
424
  except FileNotFoundError:
255
425
  pass
256
426
  except OSError as e:
257
427
  print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
258
428
 
429
+
259
430
  def save_readline_history():
260
431
  try:
261
432
  readline.write_history_file(READLINE_HISTORY_FILE)
@@ -263,52 +434,11 @@ def save_readline_history():
263
434
  print(f"Warning: Could not write readline history file {READLINE_HISTORY_FILE}: {e}")
264
435
 
265
436
 
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
437
 
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
438
 
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
439
+ valid_commands_list = list(router.routes.keys()) + list(interactive_commands.keys()) + ["cd", "exit", "quit"] + BASH_COMMANDS
440
+
310
441
 
311
- return None
312
442
 
313
443
 
314
444
  # --- Command Execution Logic ---
@@ -1146,7 +1276,6 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
1146
1276
  command_history = CommandHistory(db_path)
1147
1277
 
1148
1278
  try:
1149
- readline.set_completer(complete)
1150
1279
  history_file = setup_readline()
1151
1280
  atexit.register(save_readline_history)
1152
1281
  atexit.register(command_history.close)
@@ -1317,6 +1446,11 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1317
1446
  print(f'Using {state.current_mode} mode. Use /agent, /cmd, /chat, or /ride to switch to other modes')
1318
1447
  print(f'To switch to a different NPC, type /<npc_name>')
1319
1448
  is_windows = platform.system().lower().startswith("win")
1449
+ try:
1450
+ completer = make_completer(state)
1451
+ readline.set_completer(completer)
1452
+ except:
1453
+ pass
1320
1454
 
1321
1455
  def exit_shell(state):
1322
1456
  print("\nGoodbye!")
@@ -1336,6 +1470,12 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1336
1470
 
1337
1471
  while True:
1338
1472
  try:
1473
+ try:
1474
+ completer = make_completer(state)
1475
+ readline.set_completer(completer)
1476
+ except:
1477
+ pass
1478
+
1339
1479
  if is_windows:
1340
1480
  cwd_part = os.path.basename(state.current_path)
1341
1481
  if isinstance(state.npc, NPC):
@@ -1350,12 +1490,6 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1350
1490
  else:
1351
1491
  prompt_end = f":🤖{colored('npc', 'blue', attrs=['bold'])}{colored('sh', 'yellow')}> "
1352
1492
  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
1493
 
1360
1494
  user_input = get_multiline_input(prompt).strip()
1361
1495
  # Handle Ctrl+Z (ASCII SUB, '\x1a') as exit (Windows and Unix)
@@ -62,13 +62,11 @@ class CommandRouter:
62
62
  def __init__(self):
63
63
  self.routes = {}
64
64
  self.help_info = {}
65
- self.shell_only = {}
66
65
 
67
- def route(self, command: str, help_text: str = "", shell_only: bool = False) -> Callable:
66
+ def route(self, command: str, help_text: str = "") -> Callable:
68
67
  def wrapper(func):
69
68
  self.routes[command] = func
70
69
  self.help_info[command] = help_text
71
- self.shell_only[command] = shell_only
72
70
 
73
71
  @functools.wraps(func)
74
72
  def wrapped_func(*args, **kwargs):
@@ -102,13 +100,12 @@ router = CommandRouter()
102
100
  def get_help_text():
103
101
  commands = router.get_commands()
104
102
  help_info = router.help_info
105
- shell_only = router.shell_only
103
+
106
104
  commands.sort()
107
105
  output = "# Available Commands\n\n"
108
106
  for cmd in commands:
109
107
  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"
108
+ output += f"/{cmd} - {help_text}\n\n"
112
109
  output += """
113
110
  # Note
114
111
  - Bash commands and programs can be executed directly (try bash first, then LLM).
@@ -120,7 +117,7 @@ def get_help_text():
120
117
  def safe_get(kwargs, key, default=None):
121
118
  return kwargs.get(key, default)
122
119
 
123
- @router.route("breathe", "Condense context on a regular cadence", shell_only=True)
120
+ @router.route("breathe", "Condense context on a regular cadence")
124
121
  def breathe_handler(command: str, **kwargs):
125
122
  messages = safe_get(kwargs, "messages", [])
126
123
  npc = safe_get(kwargs, "npc")
@@ -162,7 +159,7 @@ def compile_handler(command: str, **kwargs):
162
159
 
163
160
 
164
161
 
165
- @router.route("flush", "Flush the last N messages", shell_only=True)
162
+ @router.route("flush", "Flush the last N messages")
166
163
  def flush_handler(command: str, **kwargs):
167
164
  messages = safe_get(kwargs, "messages", [])
168
165
  try:
@@ -260,6 +257,28 @@ def init_handler(command: str, **kwargs):
260
257
  traceback.print_exc()
261
258
  output = f"Error initializing project: {e}"
262
259
  return {"output": output, "messages": messages}
260
+ # Add these route handlers after the existing imports (around line 50):
261
+ @router.route("n")
262
+ @router.route("npc")
263
+ def switch_npc_handler(command: str, **kwargs) -> dict:
264
+ """Switch to a different NPC"""
265
+ team = kwargs.get('team')
266
+ parts = command.split()
267
+
268
+ if len(parts) < 2:
269
+ if team:
270
+ available_npcs = list(team.npcs.keys())
271
+ return {"output": f"Available NPCs: {', '.join(available_npcs)}"}
272
+ return {"output": "No team loaded or no NPC specified"}
273
+
274
+ npc_name = parts[1]
275
+ if team and npc_name in team.npcs:
276
+ # We can't directly modify the state here, so return a special signal
277
+ return {"output": f"SWITCH_NPC:{npc_name}"}
278
+ else:
279
+ available_npcs = list(team.npcs.keys()) if team else []
280
+ return {"output": f"NPC '{npc_name}' not found. Available: {', '.join(available_npcs)}"}
281
+
263
282
 
264
283
 
265
284
  @router.route("ots", "Take screenshot and optionally analyze with vision model")
@@ -284,7 +303,7 @@ def ots_handler(command: str, **kwargs):
284
303
  else:
285
304
  return {"output": f"Error: Image file not found at {full_path}", "messages": messages}
286
305
  else:
287
- screenshot_info = capture_screenshot(npc=npc)
306
+ screenshot_info = capture_screenshot(full=False)
288
307
  if screenshot_info and "file_path" in screenshot_info:
289
308
  image_paths.append(screenshot_info["file_path"])
290
309
  print(f"Screenshot captured: {screenshot_info.get('filename', os.path.basename(screenshot_info['file_path']))}")
@@ -823,7 +842,7 @@ def wander_handler(command: str, **kwargs):
823
842
  traceback.print_exc()
824
843
  return {"output": f"Error during wander mode: {e}", "messages": messages}
825
844
 
826
- @router.route("yap", "Enter voice chat (yap) mode", shell_only=True)
845
+ @router.route("yap", "Enter voice chat (yap) mode")
827
846
  def whisper_handler(command: str, **kwargs):
828
847
  try:
829
848
  return enter_yap_mode(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.7
3
+ Version: 1.0.9
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
@@ -84,7 +84,7 @@ extra_files = package_files("npcpy/npc_team/")
84
84
 
85
85
  setup(
86
86
  name="npcsh",
87
- version="1.0.7",
87
+ version="1.0.9",
88
88
  packages=find_packages(exclude=["tests*"]),
89
89
  install_requires=base_requirements, # Only install base requirements by default
90
90
  extras_require={
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