npcsh 1.0.7__py3-none-any.whl → 1.0.9__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 +144 -2
- npcsh/npc.py +0 -2
- npcsh/npcsh.py +195 -61
- npcsh/routes.py +29 -10
- {npcsh-1.0.7.dist-info → npcsh-1.0.9.dist-info}/METADATA +1 -1
- {npcsh-1.0.7.dist-info → npcsh-1.0.9.dist-info}/RECORD +10 -10
- {npcsh-1.0.7.dist-info → npcsh-1.0.9.dist-info}/WHEEL +0 -0
- {npcsh-1.0.7.dist-info → npcsh-1.0.9.dist-info}/entry_points.txt +0 -0
- {npcsh-1.0.7.dist-info → npcsh-1.0.9.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.0.7.dist-info → npcsh-1.0.9.dist-info}/top_level.txt +0 -0
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
|
|
@@ -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 #
|
|
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
|
|
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,
|
|
@@ -83,7 +85,178 @@ except Exception as e:
|
|
|
83
85
|
|
|
84
86
|
|
|
85
87
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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)
|
npcsh/routes.py
CHANGED
|
@@ -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 = ""
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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(
|
|
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"
|
|
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,21 +1,21 @@
|
|
|
1
1
|
npcsh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
npcsh/_state.py,sha256=
|
|
2
|
+
npcsh/_state.py,sha256=GCMUIwgIBlS7LEBLYlfBiPNKVaK19ZyxT833NFU-djU,31109
|
|
3
3
|
npcsh/alicanto.py,sha256=zJF5YwSNvtbK2EUKXzG45WOCMsSFu5cek5jCR7FgiuE,44709
|
|
4
4
|
npcsh/guac.py,sha256=Ocmk_c4NUtGsC3JOtmkbgLvD6u-XtBPRFRYcckpgUJU,33099
|
|
5
5
|
npcsh/mcp_helpers.py,sha256=Ktd2yXuBnLL2P7OMalgGLj84PXJSzaucjqmJVvWx6HA,12723
|
|
6
6
|
npcsh/mcp_npcsh.py,sha256=SfmplH62GS9iI6q4vuQLVUS6tkrok6L7JxODx_iH7ps,36158
|
|
7
7
|
npcsh/mcp_server.py,sha256=l2Ra0lpFrUu334pvp0Q9ajF2n73KvZswFi0FgbDhh9k,5884
|
|
8
|
-
npcsh/npc.py,sha256=
|
|
9
|
-
npcsh/npcsh.py,sha256=
|
|
8
|
+
npcsh/npc.py,sha256=7ujKrMQFgkeGJ4sX5Kn_dB5tjrPN58xeC91PNt453aM,7827
|
|
9
|
+
npcsh/npcsh.py,sha256=Fn6-5Ma4qYx6nDERhBbu1CgEyRM80giYKb1Gh6XALss,59793
|
|
10
10
|
npcsh/plonk.py,sha256=U2e9yUJZN95Girzzvgrh-40zOdl5zO3AHPsIjoyLv2M,15261
|
|
11
11
|
npcsh/pti.py,sha256=jGHGE5SeIcDkV8WlOEHCKQCnYAL4IPS-kUBHrUz0oDA,10019
|
|
12
|
-
npcsh/routes.py,sha256=
|
|
12
|
+
npcsh/routes.py,sha256=hYdq0gTaSNlKOa-X2w8AjnuBhku_yGIgCG2QyffdFX8,37795
|
|
13
13
|
npcsh/spool.py,sha256=GhnSFX9uAtrB4m_ijuyA5tufH12DrWdABw0z8FmiCHc,11497
|
|
14
14
|
npcsh/wander.py,sha256=BiN6eYyFnEsFzo8MFLRkdZ8xS9sTKkQpjiCcy9chMcc,23225
|
|
15
15
|
npcsh/yap.py,sha256=h5KNt9sNOrDPhGe_zfn_yFIeQhizX09zocjcPWH7m3k,20905
|
|
16
|
-
npcsh-1.0.
|
|
17
|
-
npcsh-1.0.
|
|
18
|
-
npcsh-1.0.
|
|
19
|
-
npcsh-1.0.
|
|
20
|
-
npcsh-1.0.
|
|
21
|
-
npcsh-1.0.
|
|
16
|
+
npcsh-1.0.9.dist-info/licenses/LICENSE,sha256=IKBvAECHP-aCiJtE4cHGCE5Yl0tozYz02PomGeWS3y4,1070
|
|
17
|
+
npcsh-1.0.9.dist-info/METADATA,sha256=51qElvL9UUOvqSDreAdYK3OjTjMPqgR_Tm1FqIHsov8,22747
|
|
18
|
+
npcsh-1.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
npcsh-1.0.9.dist-info/entry_points.txt,sha256=qxOYTm3ym3JWyWf2nv2Mk71uMcJIdUoNEJ8VYMkyHiY,214
|
|
20
|
+
npcsh-1.0.9.dist-info/top_level.txt,sha256=kHSNgKMCkfjV95-DH0YSp1LLBi0HXdF3w57j7MQON3E,6
|
|
21
|
+
npcsh-1.0.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|