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 +22 -2
- npcsh/alicanto.py +3 -3
- npcsh/npc.py +0 -2
- npcsh/npcsh.py +215 -90
- npcsh/routes.py +33 -13
- {npcsh-1.0.8.dist-info → npcsh-1.0.10.dist-info}/METADATA +1 -1
- npcsh-1.0.10.dist-info/RECORD +21 -0
- npcsh-1.0.8.dist-info/RECORD +0 -21
- {npcsh-1.0.8.dist-info → npcsh-1.0.10.dist-info}/WHEEL +0 -0
- {npcsh-1.0.8.dist-info → npcsh-1.0.10.dist-info}/entry_points.txt +0 -0
- {npcsh-1.0.8.dist-info → npcsh-1.0.10.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.0.8.dist-info → npcsh-1.0.10.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
|
|
@@ -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 #
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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 = ""
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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(
|
|
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"
|
|
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(
|
|
@@ -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,,
|
npcsh-1.0.8.dist-info/RECORD
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|