npcsh 1.0.26__py3-none-any.whl → 1.0.28__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.
Files changed (50) hide show
  1. npcsh/_state.py +115 -111
  2. npcsh/alicanto.py +88 -88
  3. npcsh/corca.py +423 -95
  4. npcsh/guac.py +110 -107
  5. npcsh/mcp_helpers.py +45 -45
  6. npcsh/mcp_server.py +16 -17
  7. npcsh/npc.py +16 -17
  8. npcsh/npc_team/jinxs/bash_executer.jinx +1 -1
  9. npcsh/npc_team/jinxs/edit_file.jinx +6 -6
  10. npcsh/npc_team/jinxs/image_generation.jinx +5 -5
  11. npcsh/npc_team/jinxs/screen_cap.jinx +2 -2
  12. npcsh/npcsh.py +15 -6
  13. npcsh/plonk.py +8 -8
  14. npcsh/routes.py +77 -77
  15. npcsh/spool.py +13 -13
  16. npcsh/wander.py +37 -37
  17. npcsh/yap.py +72 -72
  18. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/bash_executer.jinx +1 -1
  19. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/edit_file.jinx +6 -6
  20. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/image_generation.jinx +5 -5
  21. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/screen_cap.jinx +2 -2
  22. {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/METADATA +1 -1
  23. npcsh-1.0.28.dist-info/RECORD +73 -0
  24. npcsh-1.0.26.dist-info/RECORD +0 -73
  25. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  26. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/alicanto.png +0 -0
  27. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/corca.npc +0 -0
  28. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/corca.png +0 -0
  29. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/foreman.npc +0 -0
  30. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/frederic.npc +0 -0
  31. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/frederic4.png +0 -0
  32. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/guac.png +0 -0
  33. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/internet_search.jinx +0 -0
  34. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  35. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  36. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  37. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  38. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonk.npc +0 -0
  39. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonk.png +0 -0
  40. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  41. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  42. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/python_executor.jinx +0 -0
  43. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  44. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/sibiji.png +0 -0
  45. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/spool.png +0 -0
  46. {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/yap.png +0 -0
  47. {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/WHEEL +0 -0
  48. {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/entry_points.txt +0 -0
  49. {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/licenses/LICENSE +0 -0
  50. {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/top_level.txt +0 -0
npcsh/_state.py CHANGED
@@ -5,11 +5,18 @@ import filecmp
5
5
  import os
6
6
  from pathlib import Path
7
7
  import platform
8
- import pty
9
8
  try:
9
+ import pty
10
+ import tty
11
+
12
+ import termios
13
+
10
14
  import readline
11
15
  except:
12
- pass
16
+ readline = None
17
+ pty = None
18
+ tty = None
19
+
13
20
  import re
14
21
  import select
15
22
  import shlex
@@ -18,11 +25,8 @@ import signal
18
25
  import sqlite3
19
26
  import subprocess
20
27
  import sys
21
- from termcolor import colored
22
- import termios
23
28
  import time
24
29
  from typing import Dict, List, Any, Tuple, Union, Optional
25
- import tty
26
30
  import logging
27
31
  import textwrap
28
32
  from termcolor import colored
@@ -104,16 +108,16 @@ except importlib.metadata.PackageNotFoundError:
104
108
 
105
109
 
106
110
  NPCSH_CHAT_MODEL = os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b")
107
- # print("NPCSH_CHAT_MODEL", NPCSH_CHAT_MODEL)
111
+
108
112
  NPCSH_CHAT_PROVIDER = os.environ.get("NPCSH_CHAT_PROVIDER", "ollama")
109
- # print("NPCSH_CHAT_PROVIDER", NPCSH_CHAT_PROVIDER)
113
+
110
114
  NPCSH_DB_PATH = os.path.expanduser(
111
115
  os.environ.get("NPCSH_DB_PATH", "~/npcsh_history.db")
112
116
  )
113
117
  NPCSH_VECTOR_DB_PATH = os.path.expanduser(
114
118
  os.environ.get("NPCSH_VECTOR_DB_PATH", "~/npcsh_chroma.db")
115
119
  )
116
- #DEFAULT MODES = ['CHAT', 'AGENT', 'CODE', ]
120
+
117
121
 
118
122
  NPCSH_DEFAULT_MODE = os.path.expanduser(os.environ.get("NPCSH_DEFAULT_MODE", "agent"))
119
123
  NPCSH_VISION_MODEL = os.environ.get("NPCSH_VISION_MODEL", "gemma3:4b")
@@ -181,45 +185,45 @@ class ShellState:
181
185
  elif model_type == "video_gen":
182
186
  return self.video_gen_model, self.video_gen_provider
183
187
  else:
184
- return self.chat_model, self.chat_provider # Default fallback
188
+ return self.chat_model, self.chat_provider
185
189
  CONFIG_KEY_MAP = {
186
- # Chat
190
+
187
191
  "model": "NPCSH_CHAT_MODEL",
188
192
  "chatmodel": "NPCSH_CHAT_MODEL",
189
193
  "provider": "NPCSH_CHAT_PROVIDER",
190
194
  "chatprovider": "NPCSH_CHAT_PROVIDER",
191
195
 
192
- # Vision
196
+
193
197
  "vmodel": "NPCSH_VISION_MODEL",
194
198
  "visionmodel": "NPCSH_VISION_MODEL",
195
199
  "vprovider": "NPCSH_VISION_PROVIDER",
196
200
  "visionprovider": "NPCSH_VISION_PROVIDER",
197
201
 
198
- # Embedding
202
+
199
203
  "emodel": "NPCSH_EMBEDDING_MODEL",
200
204
  "embeddingmodel": "NPCSH_EMBEDDING_MODEL",
201
205
  "eprovider": "NPCSH_EMBEDDING_PROVIDER",
202
206
  "embeddingprovider": "NPCSH_EMBEDDING_PROVIDER",
203
207
 
204
- # Reasoning
208
+
205
209
  "rmodel": "NPCSH_REASONING_MODEL",
206
210
  "reasoningmodel": "NPCSH_REASONING_MODEL",
207
211
  "rprovider": "NPCSH_REASONING_PROVIDER",
208
212
  "reasoningprovider": "NPCSH_REASONING_PROVIDER",
209
213
 
210
- # Image generation
214
+
211
215
  "igmodel": "NPCSH_IMAGE_GEN_MODEL",
212
216
  "imagegenmodel": "NPCSH_IMAGE_GEN_MODEL",
213
217
  "igprovider": "NPCSH_IMAGE_GEN_PROVIDER",
214
218
  "imagegenprovider": "NPCSH_IMAGE_GEN_PROVIDER",
215
219
 
216
- # Video generation
220
+
217
221
  "vgmodel": "NPCSH_VIDEO_GEN_MODEL",
218
222
  "videogenmodel": "NPCSH_VIDEO_GEN_MODEL",
219
223
  "vgprovider": "NPCSH_VIDEO_GEN_PROVIDER",
220
224
  "videogenprovider": "NPCSH_VIDEO_GEN_PROVIDER",
221
225
 
222
- # Other
226
+
223
227
  "sprovider": "NPCSH_SEARCH_PROVIDER",
224
228
  "mode": "NPCSH_DEFAULT_MODE",
225
229
  "stream": "NPCSH_STREAM_OUTPUT",
@@ -233,13 +237,13 @@ def set_npcsh_config_value(key: str, value: str):
233
237
  Set NPCSH config values at runtime using shorthand (case-insensitive) or full keys.
234
238
  Updates os.environ, globals, and ShellState defaults.
235
239
  """
236
- # case-insensitive lookup for shorthand
240
+
237
241
  env_key = CONFIG_KEY_MAP.get(key.lower(), key)
238
242
 
239
- # update env
243
+
240
244
  os.environ[env_key] = value
241
245
 
242
- # normalize types
246
+
243
247
  if env_key in ["NPCSH_STREAM_OUTPUT", "NPCSH_BUILD_KG"]:
244
248
  parsed_val = value.strip().lower() in ["1", "true", "yes"]
245
249
  elif env_key.endswith("_PATH"):
@@ -247,10 +251,10 @@ def set_npcsh_config_value(key: str, value: str):
247
251
  else:
248
252
  parsed_val = value
249
253
 
250
- # update global
254
+
251
255
  globals()[env_key] = parsed_val
252
256
 
253
- # update ShellState defaults
257
+
254
258
  field_map = {
255
259
  "NPCSH_CHAT_MODEL": "chat_model",
256
260
  "NPCSH_CHAT_PROVIDER": "chat_provider",
@@ -298,7 +302,7 @@ def get_npc_path(npc_name: str, db_path: str) -> str:
298
302
  except Exception as e:
299
303
  print(f"Database query error: {e}")
300
304
 
301
- # Fallback to file paths
305
+
302
306
  if os.path.exists(project_npc_path):
303
307
  return project_npc_path
304
308
 
@@ -327,7 +331,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
327
331
  conn = sqlite3.connect(db_path)
328
332
  cursor = conn.cursor()
329
333
 
330
- # Create the compiled_npcs table if it doesn't exist
334
+
331
335
  cursor.execute(
332
336
  """
333
337
  CREATE TABLE IF NOT EXISTS compiled_npcs (
@@ -338,7 +342,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
338
342
  """
339
343
  )
340
344
 
341
- # Get the path to the npc_team directory in the package
345
+
342
346
  package_dir = os.path.dirname(__file__)
343
347
  package_npc_team_dir = os.path.join(package_dir, "npc_team")
344
348
 
@@ -368,7 +372,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
368
372
  shutil.copy2(source_path, destination_path)
369
373
  print(f"Copied ctx {filename} to {destination_path}")
370
374
 
371
- # Copy jinxs from package to user directory
375
+
372
376
  package_jinxs_dir = os.path.join(package_npc_team_dir, "jinxs")
373
377
  if os.path.exists(package_jinxs_dir):
374
378
  for filename in os.listdir(package_jinxs_dir):
@@ -416,19 +420,19 @@ def get_shell_config_file() -> str:
416
420
  Returns:
417
421
  The path to the shell configuration file.
418
422
  """
419
- # Check the current shell
423
+
420
424
  shell = os.environ.get("SHELL", "")
421
425
 
422
426
  if "zsh" in shell:
423
427
  return os.path.expanduser("~/.zshrc")
424
428
  elif "bash" in shell:
425
- # On macOS, use .bash_profile for login shells
429
+
426
430
  if platform.system() == "Darwin":
427
431
  return os.path.expanduser("~/.bash_profile")
428
432
  else:
429
433
  return os.path.expanduser("~/.bashrc")
430
434
  else:
431
- # Default to .bashrc if we can't determine the shell
435
+
432
436
  return os.path.expanduser("~/.bashrc")
433
437
 
434
438
 
@@ -569,14 +573,14 @@ def get_argument_help() -> Dict[str, List[str]]:
569
573
  arg_map = {arg: [] for arg in CANONICAL_ARGS}
570
574
 
571
575
  for arg in CANONICAL_ARGS:
572
- # Generate all possible prefixes for this argument
576
+
573
577
  for i in range(1, len(arg)):
574
578
  prefix = arg[:i]
575
579
 
576
- # Check if this prefix is an unambiguous shorthand
580
+
577
581
  matches = [canonical for canonical in CANONICAL_ARGS if canonical.startswith(prefix)]
578
582
 
579
- # If this prefix uniquely resolves to our current argument, it's a valid shorthand
583
+
580
584
  if len(matches) == 1 and matches[0] == arg:
581
585
  arg_map[arg].append(prefix)
582
586
 
@@ -668,7 +672,7 @@ BASH_COMMANDS = [
668
672
  "until",
669
673
  "wait",
670
674
  "while",
671
- # Common Unix commands
675
+
672
676
  "ls",
673
677
  "cp",
674
678
  "mv",
@@ -765,26 +769,26 @@ def start_interactive_session(command: str) -> int:
765
769
  Starts an interactive session. Only works on Unix. On Windows, print a message and return 1.
766
770
  """
767
771
  ON_WINDOWS = platform.system().lower().startswith("win")
768
- if ON_WINDOWS or termios is None or tty is None or pty is None or select is None or signal is None:
772
+ if ON_WINDOWS or termios is None or tty is None or pty is None or select is None or signal is None or tty is None:
769
773
  print("Interactive terminal sessions are not supported on Windows.")
770
774
  return 1
771
- # Save the current terminal settings
775
+
772
776
  old_tty = termios.tcgetattr(sys.stdin)
773
777
  try:
774
- # Create a pseudo-terminal
778
+
775
779
  master_fd, slave_fd = pty.openpty()
776
780
 
777
- # Start the process
781
+
778
782
  p = subprocess.Popen(
779
783
  command,
780
784
  stdin=slave_fd,
781
785
  stdout=slave_fd,
782
786
  stderr=slave_fd,
783
787
  shell=True,
784
- preexec_fn=os.setsid, # Create a new process group
788
+ preexec_fn=os.setsid,
785
789
  )
786
790
 
787
- # Set the terminal to raw mode
791
+
788
792
  tty.setraw(sys.stdin.fileno())
789
793
 
790
794
  def handle_timeout(signum, frame):
@@ -802,9 +806,9 @@ def start_interactive_session(command: str) -> int:
802
806
  else:
803
807
  break
804
808
 
805
- # Wait for the process to terminate with a timeout
809
+
806
810
  signal.signal(signal.SIGALRM, handle_timeout)
807
- signal.alarm(5) # 5 second timeout
811
+ signal.alarm(5)
808
812
  try:
809
813
  p.wait()
810
814
  except TimeoutError:
@@ -817,7 +821,7 @@ def start_interactive_session(command: str) -> int:
817
821
  signal.alarm(0)
818
822
 
819
823
  finally:
820
- # Restore the terminal settings
824
+
821
825
  termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, old_tty)
822
826
 
823
827
  return p.returncode
@@ -1009,21 +1013,21 @@ def validate_bash_command(command_parts: list) -> bool:
1009
1013
  base_command = command_parts[0]
1010
1014
 
1011
1015
  if base_command == 'which':
1012
- return False # disable which arbitrarily cause the command parsing for it is too finnicky.
1016
+ return False
1013
1017
 
1014
1018
 
1015
- # Allow interactive commands (ipython, python, sqlite3, r) as valid commands
1019
+
1016
1020
  INTERACTIVE_COMMANDS = ["ipython", "python", "sqlite3", "r"]
1017
1021
  TERMINAL_EDITORS = ["vim", "nano", "emacs"]
1018
1022
  if base_command in TERMINAL_EDITORS or base_command in INTERACTIVE_COMMANDS:
1019
1023
  return True
1020
1024
 
1021
1025
  if base_command not in COMMAND_PATTERNS and base_command not in BASH_COMMANDS:
1022
- return False # Not a recognized command
1026
+ return False
1023
1027
 
1024
1028
  pattern = COMMAND_PATTERNS.get(base_command)
1025
1029
  if not pattern:
1026
- return True # Allow commands in BASH_COMMANDS but not in COMMAND_PATTERNS
1030
+ return True
1027
1031
 
1028
1032
  args = []
1029
1033
  flags = []
@@ -1033,14 +1037,14 @@ def validate_bash_command(command_parts: list) -> bool:
1033
1037
  if part.startswith("-"):
1034
1038
  flags.append(part)
1035
1039
  if part not in pattern["flags"]:
1036
- return False # Invalid flag
1040
+ return False
1037
1041
  else:
1038
1042
  args.append(part)
1039
1043
 
1040
- # Check if 'who' has any arguments (it shouldn't)
1044
+
1041
1045
  if base_command == "who" and args:
1042
1046
  return False
1043
- # Check if any required arguments are missing
1047
+
1044
1048
  if pattern.get("requires_arg", False) and not args:
1045
1049
  return False
1046
1050
 
@@ -1077,7 +1081,7 @@ def execute_set_command(command: str, value: str) -> str:
1077
1081
 
1078
1082
  config_path = os.path.expanduser("~/.npcshrc")
1079
1083
 
1080
- # Map command to environment variable name
1084
+
1081
1085
  var_map = {
1082
1086
  "model": "NPCSH_CHAT_MODEL",
1083
1087
  "provider": "NPCSH_CHAT_PROVIDER",
@@ -1089,14 +1093,14 @@ def execute_set_command(command: str, value: str) -> str:
1089
1093
 
1090
1094
  env_var = var_map[command]
1091
1095
 
1092
- # Read the current configuration
1096
+
1093
1097
  if os.path.exists(config_path):
1094
1098
  with open(config_path, "r") as f:
1095
1099
  lines = f.readlines()
1096
1100
  else:
1097
1101
  lines = []
1098
1102
 
1099
- # Check if the property exists and update it, or add it if it doesn't exist
1103
+
1100
1104
  property_exists = False
1101
1105
  for i, line in enumerate(lines):
1102
1106
  if line.startswith(f"export {env_var}="):
@@ -1107,7 +1111,7 @@ def execute_set_command(command: str, value: str) -> str:
1107
1111
  if not property_exists:
1108
1112
  lines.append(f"export {env_var}='{value}'\n")
1109
1113
 
1110
- # Save the updated configuration
1114
+
1111
1115
  with open(config_path, "w") as f:
1112
1116
  f.writelines(lines)
1113
1117
 
@@ -1139,7 +1143,7 @@ def set_npcsh_initialized() -> None:
1139
1143
  npcshrc.write(content)
1140
1144
  npcshrc.truncate()
1141
1145
 
1142
- # Also set it for the current session
1146
+
1143
1147
  os.environ["NPCSH_INITIALIZED"] = "1"
1144
1148
  print("NPCSH initialization flag set in .npcshrc")
1145
1149
 
@@ -1158,7 +1162,7 @@ def file_has_changed(source_path: str, destination_path: str) -> bool:
1158
1162
  A boolean indicating whether the files are different
1159
1163
  """
1160
1164
 
1161
- # Compare file modification times or contents to decide whether to update the file
1165
+
1162
1166
  return not filecmp.cmp(source_path, destination_path, shallow=False)
1163
1167
 
1164
1168
 
@@ -1245,7 +1249,7 @@ def read_rc_file_windows(path):
1245
1249
  for line in f:
1246
1250
  line = line.strip()
1247
1251
  if line and not line.startswith("#"):
1248
- # Match KEY='value' or KEY="value" format
1252
+
1249
1253
  match = re.match(r'^([A-Z_]+)\s*=\s*[\'"](.*?)[\'"]$', line)
1250
1254
  if match:
1251
1255
  key, value = match.groups()
@@ -1254,11 +1258,11 @@ def read_rc_file_windows(path):
1254
1258
 
1255
1259
 
1256
1260
  def get_setting_windows(key, default=None):
1257
- # Try environment variable first
1261
+
1258
1262
  if env_value := os.getenv(key):
1259
1263
  return env_value
1260
1264
 
1261
- # Fall back to .npcshrc file
1265
+
1262
1266
  config = read_rc_file_windows(get_npcshrc_path_windows())
1263
1267
  return config.get(key, default)
1264
1268
 
@@ -1303,7 +1307,7 @@ READLINE_HISTORY_FILE = os.path.expanduser("~/.npcsh_readline_history")
1303
1307
  DEFAULT_NPC_TEAM_PATH = os.path.expanduser("~/.npcsh/npc_team/")
1304
1308
  PROJECT_NPC_TEAM_PATH = "./npc_team/"
1305
1309
 
1306
- # --- Global Clients ---
1310
+
1307
1311
  try:
1308
1312
  chroma_client = chromadb.PersistentClient(path=EMBEDDINGS_DB_PATH) if chromadb else None
1309
1313
  except Exception as e:
@@ -1333,11 +1337,11 @@ def get_path_executables() -> List[str]:
1333
1337
 
1334
1338
  import logging
1335
1339
 
1336
- # Set up completion logger
1340
+
1337
1341
  completion_logger = logging.getLogger('npcsh.completion')
1338
- completion_logger.setLevel(logging.WARNING) # Default to WARNING (quiet)
1342
+ completion_logger.setLevel(logging.WARNING)
1343
+
1339
1344
 
1340
- # Add handler if not already present
1341
1345
  if not completion_logger.handlers:
1342
1346
  handler = logging.StreamHandler(sys.stderr)
1343
1347
  formatter = logging.Formatter('[%(name)s] %(message)s')
@@ -1356,7 +1360,7 @@ def make_completer(shell_state: ShellState, router: Any):
1356
1360
 
1357
1361
  matches = []
1358
1362
 
1359
- # Check if we're completing a slash command
1363
+
1360
1364
  if begidx > 0 and buffer[begidx-1] == '/':
1361
1365
  completion_logger.debug(f"Slash command completion - text='{text}'")
1362
1366
  slash_commands = get_slash_commands(shell_state, router)
@@ -1414,19 +1418,19 @@ def get_slash_commands(state: ShellState, router: Any) -> List[str]:
1414
1418
  commands.extend(router_cmds)
1415
1419
  completion_logger.debug(f"Router commands: {router_cmds}")
1416
1420
 
1417
- # Team jinxs
1421
+
1418
1422
  if state.team and hasattr(state.team, 'jinxs_dict'):
1419
1423
  jinx_cmds = [f"/{jinx}" for jinx in state.team.jinxs_dict.keys()]
1420
1424
  commands.extend(jinx_cmds)
1421
1425
  completion_logger.debug(f"Jinx commands: {jinx_cmds}")
1422
1426
 
1423
- # NPC names for switching
1427
+
1424
1428
  if state.team and hasattr(state.team, 'npcs'):
1425
1429
  npc_cmds = [f"/{npc}" for npc in state.team.npcs.keys()]
1426
1430
  commands.extend(npc_cmds)
1427
1431
  completion_logger.debug(f"NPC commands: {npc_cmds}")
1428
1432
 
1429
- # Mode switching commands
1433
+
1430
1434
  mode_cmds = ['/cmd', '/agent', '/chat']
1431
1435
  commands.extend(mode_cmds)
1432
1436
  completion_logger.debug(f"Mode commands: {mode_cmds}")
@@ -1460,7 +1464,7 @@ def get_file_completions(text: str) -> List[str]:
1460
1464
  else:
1461
1465
  completion = os.path.join(basedir, item)
1462
1466
 
1463
- # Just return the name, let readline handle spacing/slashes
1467
+
1464
1468
  matches.append(completion)
1465
1469
  except (PermissionError, OSError):
1466
1470
  pass
@@ -1470,15 +1474,15 @@ def get_file_completions(text: str) -> List[str]:
1470
1474
  return []
1471
1475
  def is_command_position(buffer: str, begidx: int) -> bool:
1472
1476
  """Determine if cursor is at a command position"""
1473
- # Get the part of buffer before the current word
1477
+
1474
1478
  before_word = buffer[:begidx]
1475
1479
 
1476
- # Split by command separators
1480
+
1477
1481
  parts = re.split(r'[|;&]', before_word)
1478
1482
  current_command_part = parts[-1].strip()
1479
1483
 
1480
- # If there's nothing before the current word in this command part,
1481
- # or only whitespace, we're at command position
1484
+
1485
+
1482
1486
  return len(current_command_part) == 0
1483
1487
 
1484
1488
 
@@ -1608,10 +1612,10 @@ def format_file_listing(output: str) -> str:
1608
1612
  colored_filepath = colored(filepath_guess, color, attrs=attrs)
1609
1613
 
1610
1614
  if len(parts) > 1 :
1611
- # Handle cases like 'ls -l' where filename is last
1615
+
1612
1616
  colored_line = " ".join(parts[:-1] + [colored_filepath])
1613
1617
  else:
1614
- # Handle cases where line is just the filename
1618
+
1615
1619
  colored_line = colored_filepath
1616
1620
 
1617
1621
  colored_lines.append(colored_line)
@@ -1627,7 +1631,7 @@ def wrap_text(text: str, width: int = 80) -> str:
1627
1631
  lines.append(paragraph)
1628
1632
  return "\n".join(lines)
1629
1633
 
1630
- # --- Readline Setup and Completion ---
1634
+
1631
1635
 
1632
1636
  def setup_readline() -> str:
1633
1637
  """Setup readline with history and completion"""
@@ -1635,7 +1639,7 @@ def setup_readline() -> str:
1635
1639
  readline.read_history_file(READLINE_HISTORY_FILE)
1636
1640
  readline.set_history_length(1000)
1637
1641
 
1638
- # Don't set completer here - it will be set in run_repl with state
1642
+
1639
1643
  readline.parse_and_bind("tab: complete")
1640
1644
 
1641
1645
  readline.parse_and_bind("set enable-bracketed-paste on")
@@ -1666,7 +1670,7 @@ def store_command_embeddings(command: str, output: Any, state: ShellState):
1666
1670
 
1667
1671
  try:
1668
1672
  output_str = str(output) if output else ""
1669
- if not command and not output_str: return # Avoid empty embeddings
1673
+ if not command and not output_str: return
1670
1674
 
1671
1675
  texts_to_embed = [command, output_str]
1672
1676
 
@@ -1716,7 +1720,7 @@ def handle_interactive_command(cmd_parts: List[str], state: ShellState) -> Tuple
1716
1720
  command_name = cmd_parts[0]
1717
1721
  print(f"Starting interactive {command_name} session...")
1718
1722
  try:
1719
- # CORRECTED: Join all parts into one string to pass to the function.
1723
+
1720
1724
  full_command_str = " ".join(cmd_parts)
1721
1725
  return_code = start_interactive_session(full_command_str)
1722
1726
  output = f"Interactive {command_name} session ended with return code {return_code}"
@@ -1735,7 +1739,7 @@ def handle_cd_command(cmd_parts: List[str], state: ShellState) -> Tuple[ShellSta
1735
1739
  output = colored(f"cd: no such file or directory: {target_path}", "red")
1736
1740
  except Exception as e:
1737
1741
  output = colored(f"cd: error changing directory: {e}", "red")
1738
- os.chdir(original_path) # Revert if error
1742
+ os.chdir(original_path)
1739
1743
 
1740
1744
  return state, output
1741
1745
 
@@ -1806,21 +1810,21 @@ def parse_generic_command_flags(parts: List[str]) -> Tuple[Dict[str, Any], List[
1806
1810
  key, value = key_part.split('=', 1)
1807
1811
  parsed_kwargs[key] = _try_convert_type(value)
1808
1812
  else:
1809
- # Look ahead for a value
1813
+
1810
1814
  if i + 1 < len(parts) and not parts[i + 1].startswith('-'):
1811
1815
  parsed_kwargs[key_part] = _try_convert_type(parts[i + 1])
1812
- i += 1 # Consume the value
1816
+ i += 1
1813
1817
  else:
1814
- parsed_kwargs[key_part] = True # Boolean flag
1818
+ parsed_kwargs[key_part] = True
1815
1819
 
1816
1820
  elif part.startswith('-'):
1817
1821
  key = part[1:]
1818
- # Look ahead for a value
1822
+
1819
1823
  if i + 1 < len(parts) and not parts[i + 1].startswith('-'):
1820
1824
  parsed_kwargs[key] = _try_convert_type(parts[i + 1])
1821
- i += 1 # Consume the value
1825
+ i += 1
1822
1826
  else:
1823
- parsed_kwargs[key] = True # Boolean flag
1827
+ parsed_kwargs[key] = True
1824
1828
 
1825
1829
  elif '=' in part and not part.startswith('-'):
1826
1830
  key, value = part.split('=', 1)
@@ -1837,7 +1841,7 @@ def parse_generic_command_flags(parts: List[str]) -> Tuple[Dict[str, Any], List[
1837
1841
  def should_skip_kg_processing(user_input: str, assistant_output: str) -> bool:
1838
1842
  """Determine if this interaction is too trivial for KG processing"""
1839
1843
 
1840
- # Skip if user input is very short (less than 10 chars)
1844
+
1841
1845
  if len(user_input.strip()) < 10:
1842
1846
  return True
1843
1847
 
@@ -1866,7 +1870,7 @@ def execute_slash_command(command: str,
1866
1870
  all_command_parts = shlex.split(command)
1867
1871
  command_name = all_command_parts[0].lstrip('/')
1868
1872
 
1869
- # Handle NPC switching commands
1873
+
1870
1874
  if command_name in ['n', 'npc']:
1871
1875
  npc_to_switch_to = all_command_parts[1] if len(all_command_parts) > 1 else None
1872
1876
  if npc_to_switch_to and state.team and npc_to_switch_to in state.team.npcs:
@@ -1876,7 +1880,7 @@ def execute_slash_command(command: str,
1876
1880
  available_npcs = list(state.team.npcs.keys()) if state.team else []
1877
1881
  return state, colored(f"NPC '{npc_to_switch_to}' not found. Available NPCs: {', '.join(available_npcs)}", "red")
1878
1882
 
1879
- # Check router commands first
1883
+
1880
1884
  handler = router.get_route(command_name)
1881
1885
  if handler:
1882
1886
  parsed_flags, positional_args = parse_generic_command_flags(all_command_parts[1:])
@@ -1892,12 +1896,12 @@ def execute_slash_command(command: str,
1892
1896
  'positional_args': positional_args,
1893
1897
  'plonk_context': state.team.shared_context.get('PLONK_CONTEXT') if state.team and hasattr(state.team, 'shared_context') else None,
1894
1898
 
1895
- # Default chat model/provider
1899
+
1896
1900
  'model': state.npc.model if isinstance(state.npc, NPC) and state.npc.model else state.chat_model,
1897
1901
  'provider': state.npc.provider if isinstance(state.npc, NPC) and state.npc.provider else state.chat_provider,
1898
1902
  'npc': state.npc,
1899
1903
 
1900
- # All other specific defaults
1904
+
1901
1905
  'sprovider': state.search_provider,
1902
1906
  'emodel': state.embedding_model,
1903
1907
  'eprovider': state.embedding_provider,
@@ -1918,7 +1922,7 @@ def execute_slash_command(command: str,
1918
1922
 
1919
1923
  render_markdown(f'- Calling {command_name} handler {kwarg_part} ')
1920
1924
 
1921
- # Handle model/provider inference
1925
+
1922
1926
  if 'model' in normalized_flags and 'provider' not in normalized_flags:
1923
1927
  inferred_provider = lookup_provider(normalized_flags['model'])
1924
1928
  if inferred_provider:
@@ -1947,7 +1951,7 @@ def execute_slash_command(command: str,
1947
1951
  traceback.print_exc()
1948
1952
  return state, colored(f"Error executing slash command '{command_name}': {e}", "red")
1949
1953
 
1950
- # Check for jinxs in active NPC
1954
+
1951
1955
  active_npc = state.npc if isinstance(state.npc, NPC) else None
1952
1956
  jinx_to_execute = None
1953
1957
  executor = None
@@ -1959,16 +1963,16 @@ def execute_slash_command(command: str,
1959
1963
  jinx_to_execute = state.team.jinxs_dict[command_name]
1960
1964
  executor = state.team
1961
1965
  if jinx_to_execute:
1962
- args = all_command_parts[1:] # Fix: use all_command_parts instead of command_parts
1966
+ args = all_command_parts[1:]
1963
1967
  try:
1964
- # Create input dictionary from args based on jinx inputs
1968
+
1965
1969
  input_values = {}
1966
1970
  if hasattr(jinx_to_execute, 'inputs') and jinx_to_execute.inputs:
1967
1971
  for i, input_name in enumerate(jinx_to_execute.inputs):
1968
1972
  if i < len(args):
1969
1973
  input_values[input_name] = args[i]
1970
1974
 
1971
- # Execute the jinx with proper parameters
1975
+
1972
1976
  if isinstance(executor, NPC):
1973
1977
  jinx_output = jinx_to_execute.execute(
1974
1978
  input_values=input_values,
@@ -1976,7 +1980,7 @@ def execute_slash_command(command: str,
1976
1980
  npc=executor,
1977
1981
  messages=state.messages
1978
1982
  )
1979
- else: # Team executor
1983
+ else:
1980
1984
  jinx_output = jinx_to_execute.execute(
1981
1985
  input_values=input_values,
1982
1986
  jinxs_dict=executor.jinxs_dict if hasattr(executor, 'jinxs_dict') else {},
@@ -2097,7 +2101,7 @@ def process_pipeline_command(
2097
2101
  stream=stream_final,
2098
2102
  context=info,
2099
2103
  )
2100
- #
2104
+
2101
2105
 
2102
2106
  if not review:
2103
2107
  if isinstance(llm_result, dict):
@@ -2131,7 +2135,7 @@ def review_and_iterate_command(
2131
2135
  Simple iteration on LLM command result to improve quality.
2132
2136
  """
2133
2137
 
2134
- # Extract current state
2138
+
2135
2139
  if isinstance(initial_result, dict):
2136
2140
  current_output = initial_result.get("output")
2137
2141
  current_messages = initial_result.get("messages", state.messages)
@@ -2139,7 +2143,7 @@ def review_and_iterate_command(
2139
2143
  current_output = initial_result
2140
2144
  current_messages = state.messages
2141
2145
 
2142
- # Simple refinement prompt
2146
+
2143
2147
  refinement_prompt = f"""
2144
2148
  The previous response to "{original_command}" was:
2145
2149
  {current_output}
@@ -2147,7 +2151,7 @@ The previous response to "{original_command}" was:
2147
2151
  Please review and improve this response if needed. Provide a better, more complete answer.
2148
2152
  """
2149
2153
 
2150
- # Iterate with check_llm_command
2154
+
2151
2155
  refined_result = check_llm_command(
2152
2156
  refinement_prompt,
2153
2157
  model=exec_model,
@@ -2162,7 +2166,7 @@ Please review and improve this response if needed. Provide a better, more comple
2162
2166
  context=info,
2163
2167
  )
2164
2168
 
2165
- # Update state and return
2169
+
2166
2170
  if isinstance(refined_result, dict):
2167
2171
  state.messages = refined_result.get("messages", current_messages)
2168
2172
  return state, refined_result.get("output", current_output)
@@ -2198,8 +2202,8 @@ def execute_command(
2198
2202
  active_model = npc_model or state.chat_model
2199
2203
  active_provider = npc_provider or state.chat_provider
2200
2204
  if state.current_mode == 'agent':
2201
- #print('# of parsed commands: ', len(commands))
2202
- #print('Commands:' '\n'.join(commands))
2205
+
2206
+
2203
2207
  for i, cmd_segment in enumerate(commands):
2204
2208
  render_markdown(f'- Executing command {i+1}/{len(commands)}')
2205
2209
  is_last_command = (i == len(commands) - 1)
@@ -2234,7 +2238,7 @@ def execute_command(
2234
2238
  except Exception:
2235
2239
  print(f"Warning: Cannot convert output to string for piping: {type(output)}", file=sys.stderr)
2236
2240
  stdin_for_next = None
2237
- else: # Output was None
2241
+ else:
2238
2242
  stdin_for_next = None
2239
2243
  except Exception as pipeline_error:
2240
2244
  import traceback
@@ -2249,7 +2253,7 @@ def execute_command(
2249
2253
 
2250
2254
 
2251
2255
  elif state.current_mode == 'chat':
2252
- # Only treat as bash if it looks like a shell command (starts with known command or is a slash command)
2256
+
2253
2257
  cmd_parts = parse_command_safely(command)
2254
2258
  is_probably_bash = (
2255
2259
  cmd_parts
@@ -2274,9 +2278,9 @@ def execute_command(
2274
2278
  except Exception as bash_err:
2275
2279
  return state, colored(f"Bash execution failed: {bash_err}", "red")
2276
2280
  except Exception:
2277
- pass # Fall through to LLM
2281
+ pass
2278
2282
 
2279
- # Otherwise, treat as chat (LLM)
2283
+
2280
2284
  response = get_llm_response(
2281
2285
  command,
2282
2286
  model=active_model,
@@ -2390,7 +2394,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
2390
2394
  print(f"Warning: Could not load context file {filename}: {e}")
2391
2395
 
2392
2396
  forenpc_name = team_ctx.get("forenpc", default_forenpc_name)
2393
- #render_markdown(f"- Using forenpc: {forenpc_name}")
2397
+
2394
2398
 
2395
2399
  if team_ctx.get("use_global_jinxs", False):
2396
2400
  jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
@@ -2404,7 +2408,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
2404
2408
  forenpc_path = os.path.join(team_dir, f"{forenpc_name}.npc")
2405
2409
 
2406
2410
 
2407
- #render_markdown('- Loaded team context'+ json.dumps(team_ctx, indent=2))
2411
+
2408
2412
 
2409
2413
 
2410
2414
 
@@ -2430,7 +2434,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
2430
2434
  if not npc_obj.provider:
2431
2435
  npc_obj.provider = initial_state.chat_provider
2432
2436
 
2433
- # Also apply to the forenpc specifically
2437
+
2434
2438
  if team.forenpc and isinstance(team.forenpc, NPC):
2435
2439
  if not team.forenpc.model:
2436
2440
  team.forenpc.model = initial_state.chat_model
@@ -2442,7 +2446,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
2442
2446
  elif team_dir and os.path.basename(team_dir) != 'npc_team':
2443
2447
  team.name = os.path.basename(team_dir)
2444
2448
  else:
2445
- team.name = "global_team" # fallback for ~/.npcsh/npc_team
2449
+ team.name = "global_team"
2446
2450
 
2447
2451
  return command_history, team, forenpc_obj
2448
2452
 
@@ -2460,7 +2464,7 @@ def process_result(
2460
2464
  team_name = result_state.team.name if result_state.team else "__none__"
2461
2465
  npc_name = result_state.npc.name if isinstance(result_state.npc, NPC) else "__none__"
2462
2466
 
2463
- # Determine the actual NPC object to use for this turn's operations
2467
+
2464
2468
  active_npc = result_state.npc if isinstance(result_state.npc, NPC) else NPC(
2465
2469
  name="default",
2466
2470
  model=result_state.chat_model,
@@ -2543,7 +2547,7 @@ def process_result(
2543
2547
  except Exception as e:
2544
2548
  print(colored(f"Error during real-time KG evolution: {e}", "red"))
2545
2549
 
2546
- # --- Part 3: Periodic Team Context Suggestions ---
2550
+
2547
2551
  result_state.turn_count += 1
2548
2552
 
2549
2553
  if result_state.turn_count > 0 and result_state.turn_count % 10 == 0: