npcsh 1.0.3__tar.gz → 1.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: npcsh is a command-line toolkit for using AI agents in novel ways.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcsh
6
6
  Author: Christopher Agostino
@@ -411,7 +411,6 @@ BASH_COMMANDS = [
411
411
  "info",
412
412
  "whatis",
413
413
  "whereis",
414
- "which",
415
414
  "date",
416
415
  "cal",
417
416
  "bc",
@@ -548,17 +547,21 @@ def validate_bash_command(command_parts: list) -> bool:
548
547
  "--count",
549
548
  "--heading",
550
549
  ],
551
- "requires_arg": True,
550
+ "requires_arg": False,
552
551
  },
553
552
  "open": {
554
553
  "flags": ["-a", "-e", "-t", "-f", "-F", "-W", "-n", "-g", "-h"],
555
554
  "requires_arg": True,
556
555
  },
557
- "which": {"flags": ["-a", "-s", "-v"], "requires_arg": True},
556
+
558
557
  }
559
558
 
560
559
  base_command = command_parts[0]
561
560
 
561
+ if base_command == 'which':
562
+ return False # disable which arbitrarily cause the command parsing for it is too finnicky.
563
+
564
+
562
565
  if base_command not in COMMAND_PATTERNS:
563
566
  return True # Allow other commands to pass through
564
567
 
@@ -578,11 +581,6 @@ def validate_bash_command(command_parts: list) -> bool:
578
581
  # Check if 'who' has any arguments (it shouldn't)
579
582
  if base_command == "who" and args:
580
583
  return False
581
-
582
- # Handle 'which' with '-a' flag
583
- if base_command == "which" and "-a" in flags:
584
- return True # Allow 'which -a' with or without arguments.
585
-
586
584
  # Check if any required arguments are missing
587
585
  if pattern.get("requires_arg", False) and not args:
588
586
  return False
@@ -37,7 +37,7 @@ from npcsh._state import (
37
37
  interactive_commands,
38
38
  BASH_COMMANDS,
39
39
  start_interactive_session,
40
-
40
+ validate_bash_command
41
41
  )
42
42
 
43
43
  from npcpy.npc_sysenv import (
@@ -81,9 +81,6 @@ except Exception as e:
81
81
  print(f"Warning: Failed to initialize ChromaDB client at {EMBEDDINGS_DB_PATH}: {e}")
82
82
  chroma_client = None
83
83
 
84
- # --- Custom Exceptions ---
85
- class CommandNotFoundError(Exception):
86
- pass
87
84
 
88
85
 
89
86
  from npcsh._state import initial_state, ShellState
@@ -404,14 +401,7 @@ def handle_bash_command(
404
401
  cmd_str: str,
405
402
  stdin_input: Optional[str],
406
403
  state: ShellState,
407
- ) -> Tuple[ShellState, str]:
408
-
409
- command_name = cmd_parts[0]
410
-
411
- if command_name in TERMINAL_EDITORS:
412
- output = open_terminal_editor(cmd_str)
413
- return state, output
414
-
404
+ ) -> Tuple[bool, str]:
415
405
  try:
416
406
  process = subprocess.Popen(
417
407
  cmd_parts,
@@ -421,39 +411,23 @@ def handle_bash_command(
421
411
  text=True,
422
412
  cwd=state.current_path
423
413
  )
424
-
425
414
  stdout, stderr = process.communicate(input=stdin_input)
426
415
 
427
416
  if process.returncode != 0:
428
- err_msg = stderr.strip() if stderr else f"Command '{cmd_str}' failed with return code {process.returncode}."
429
- # If it failed because command not found, raise specific error for fallback
430
- if "No such file or directory" in err_msg or "command not found" in err_msg:
431
- raise CommandNotFoundError(err_msg)
432
- # Otherwise, return the error output
433
- full_output = stdout.strip() + ("\n" + colored(f"stderr: {err_msg}", "red") if err_msg else "")
434
- return state, full_output.strip()
435
-
436
-
437
- output = stdout.strip() if stdout else ""
438
- if stderr:
439
- # Log stderr but don't necessarily include in piped output unless requested
440
- print(colored(f"stderr: {stderr.strip()}", "yellow"), file=sys.stderr)
417
+ return False, stderr.strip() if stderr else f"Command '{cmd_str}' failed with return code {process.returncode}."
441
418
 
419
+ if stderr.strip():
420
+ print(colored(f"stderr: {stderr.strip()}", "yellow"), file=sys.stderr)
421
+
422
+ if cmd_parts[0] in ["ls", "find", "dir"]:
423
+ return True, format_file_listing(stdout.strip())
442
424
 
443
- if command_name in ["ls", "find", "dir"]:
444
- output = format_file_listing(output)
445
- elif not output and process.returncode == 0 and not stderr:
446
- output = "" # No output is valid, don't print success message if piping
447
-
448
- return state, output
425
+ return True, stdout.strip()
449
426
 
450
427
  except FileNotFoundError:
451
- raise CommandNotFoundError(f"Command not found: {command_name}")
452
- except PermissionError as e:
453
- return state, colored(f"Error executing '{cmd_str}': Permission denied. {e}", "red")
454
- except Exception as e:
455
- return state, colored(f"Error executing command '{cmd_str}': {e}", "red")
456
-
428
+ return False, f"Command not found: {cmd_parts[0]}"
429
+ except PermissionError:
430
+ return False, f"Permission denied: {cmd_str}"
457
431
 
458
432
  def execute_slash_command(command: str, stdin_input: Optional[str], state: ShellState, stream: bool) -> Tuple[ShellState, Any]:
459
433
  """Executes slash commands using the router or checking NPC/Team jinxs."""
@@ -527,7 +501,6 @@ def execute_slash_command(command: str, stdin_input: Optional[str], state: Shell
527
501
 
528
502
  return state, colored(f"Unknown slash command or jinx: {command_name}", "red")
529
503
 
530
-
531
504
  def process_pipeline_command(
532
505
  cmd_segment: str,
533
506
  stdin_input: Optional[str],
@@ -553,84 +526,60 @@ def process_pipeline_command(
553
526
  if cmd_to_process.startswith("/"):
554
527
  return execute_slash_command(cmd_to_process, stdin_input, state, stream_final)
555
528
 
556
- try:
557
- cmd_parts = parse_command_safely(cmd_to_process)
558
- if not cmd_parts:
559
- return state, stdin_input
529
+ cmd_parts = parse_command_safely(cmd_to_process)
530
+ if not cmd_parts:
531
+ return state, stdin_input
560
532
 
533
+ if validate_bash_command(cmd_parts):
561
534
  command_name = cmd_parts[0]
562
-
563
- is_unambiguous_bash = (
564
- command_name in BASH_COMMANDS or
565
- command_name in interactive_commands or
566
- command_name == "cd" or
567
- cmd_to_process.startswith("./")
568
- )
569
-
570
- if is_unambiguous_bash:
571
- if command_name in interactive_commands:
572
- return handle_interactive_command(cmd_parts, state)
573
- elif command_name == "cd":
574
- return handle_cd_command(cmd_parts, state)
575
- else:
576
- return handle_bash_command(cmd_parts, cmd_to_process, stdin_input, state)
535
+ if command_name in interactive_commands:
536
+ return handle_interactive_command(cmd_parts, state)
537
+ if command_name == "cd":
538
+ return handle_cd_command(cmd_parts, state)
539
+
540
+ success, result = handle_bash_command(cmd_parts, cmd_to_process, stdin_input, state)
541
+ if success:
542
+ return state, result
577
543
  else:
578
- full_llm_cmd = f"{cmd_to_process} {stdin_input}" if stdin_input else cmd_to_process
579
-
580
- path_cmd = 'The current working directory is: ' + state.current_path
581
- ls_files = 'Files in the current directory (full paths):\n' + "\n".join([os.path.join(state.current_path, f) for f in os.listdir(state.current_path)]) if os.path.exists(state.current_path) else 'No files found in the current directory.'
582
- platform_info = f"Platform: {platform.system()} {platform.release()} ({platform.machine()})"
583
- full_llm_cmd = path_cmd + '\n' + ls_files + '\n' + platform_info + '\n' + full_llm_cmd
584
- llm_result = check_llm_command(
585
- full_llm_cmd,
586
- model=exec_model,
587
- provider=exec_provider,
588
- api_url=state.api_url,
589
- api_key=state.api_key,
590
- npc=state.npc,
591
- team=state.team,
592
- messages=state.messages,
593
- images=state.attachments,
594
- stream=stream_final,
595
- context=None,
596
- shell=True,
544
+ print(colored(f"Bash command failed. Asking LLM for a fix: {result}", "yellow"), file=sys.stderr)
545
+ fixer_prompt = f"The command '{cmd_to_process}' failed with the error: '{result}'. Provide the correct command."
546
+ response = execute_llm_command(
547
+ fixer_prompt,
548
+ model=exec_model,
549
+ provider=exec_provider,
550
+ npc=state.npc,
551
+ stream=stream_final,
552
+ messages=state.messages
597
553
  )
598
- if isinstance(llm_result, dict):
599
- state.messages = llm_result.get("messages", state.messages)
600
- output = llm_result.get("output")
601
- return state, output
602
- else:
603
- return state, llm_result
604
-
605
- except CommandNotFoundError as e:
606
- print(colored(f"Command not found, falling back to LLM: {e}", "yellow"), file=sys.stderr)
554
+ state.messages = response['messages']
555
+ return state, response['response']
556
+ else:
607
557
  full_llm_cmd = f"{cmd_to_process} {stdin_input}" if stdin_input else cmd_to_process
558
+ path_cmd = 'The current working directory is: ' + state.current_path
559
+ ls_files = 'Files in the current directory (full paths):\n' + "\n".join([os.path.join(state.current_path, f) for f in os.listdir(state.current_path)]) if os.path.exists(state.current_path) else 'No files found in the current directory.'
560
+ platform_info = f"Platform: {platform.system()} {platform.release()} ({platform.machine()})"
561
+ info = path_cmd + '\n' + ls_files + '\n' + platform_info + '\n'
562
+
608
563
  llm_result = check_llm_command(
609
- full_llm_cmd,
610
- model=exec_model,
564
+ full_llm_cmd,
565
+ model=exec_model,
611
566
  provider=exec_provider,
612
- api_url=state.api_url,
613
- api_key=state.api_key,
567
+ api_url=state.api_url,
568
+ api_key=state.api_key,
614
569
  npc=state.npc,
615
- team=state.team,
616
- messages=state.messages,
570
+ team=state.team,
571
+ messages=state.messages,
617
572
  images=state.attachments,
618
- stream=stream_final,
619
- context=None,
620
- shell=True
573
+ stream=stream_final,
574
+ context=info,
575
+ shell=True,
621
576
  )
622
577
  if isinstance(llm_result, dict):
623
578
  state.messages = llm_result.get("messages", state.messages)
624
579
  output = llm_result.get("output")
625
580
  return state, output
626
581
  else:
627
- return state, llm_result
628
-
629
- except Exception as e:
630
- import traceback
631
- traceback.print_exc()
632
- return state, colored(f"Error processing command '{cmd_segment[:50]}...': {e}", "red")
633
-
582
+ return state, llm_result
634
583
  def check_mode_switch(command:str , state: ShellState):
635
584
  if command in ['/cmd', '/agent', '/chat', '/ride']:
636
585
  state.current_mode = command[1:]
@@ -729,8 +678,6 @@ def execute_command(
729
678
  try:
730
679
  bash_state, bash_output = handle_bash_command(cmd_parts, command, None, state)
731
680
  return bash_state, bash_output
732
- except CommandNotFoundError:
733
- pass # Fall through to LLM
734
681
  except Exception as bash_err:
735
682
  return state, colored(f"Bash execution failed: {bash_err}", "red")
736
683
  except Exception:
@@ -1215,46 +1162,51 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
1215
1162
  team_dir = project_team_path
1216
1163
  default_forenpc_name = "forenpc"
1217
1164
  else:
1218
- resp = input(f"No npc_team found in {os.getcwd()}. Create a new team here? [Y/n]: ").strip().lower()
1219
- if resp in ("", "y", "yes"):
1220
- team_dir = project_team_path
1221
- os.makedirs(team_dir, exist_ok=True)
1222
- default_forenpc_name = "forenpc"
1223
- forenpc_directive = input(
1224
- f"Enter a primary directive for {default_forenpc_name} (default: 'You are the forenpc of the team...'): "
1225
- ).strip() or "You are the forenpc of the team, coordinating activities between NPCs on the team, verifying that results from NPCs are high quality and can help to adequately answer user requests."
1226
- forenpc_model = input("Enter a model for your forenpc (default: llama3.2): ").strip() or "llama3.2"
1227
- forenpc_provider = input("Enter a provider for your forenpc (default: ollama): ").strip() or "ollama"
1228
-
1229
- with open(os.path.join(team_dir, f"{default_forenpc_name}.npc"), "w") as f:
1230
- yaml.dump({
1231
- "name": default_forenpc_name, "primary_directive": forenpc_directive,
1232
- "model": forenpc_model, "provider": forenpc_provider
1233
- }, f)
1234
-
1235
- ctx_path = os.path.join(team_dir, "team.ctx")
1236
- folder_context = input("Enter a short description for this project/team (optional): ").strip()
1237
- team_ctx_data = {
1238
- "forenpc": default_forenpc_name, "model": forenpc_model,
1239
- "provider": forenpc_provider, "api_key": None, "api_url": None,
1240
- "context": folder_context if folder_context else None
1241
- }
1242
- use_jinxs = input("Use global jinxs folder (g) or copy to this project (c)? [g/c, default: g]: ").strip().lower()
1243
- if use_jinxs == "c":
1244
- global_jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
1245
- if os.path.exists(global_jinxs_dir):
1246
- shutil.copytree(global_jinxs_dir, os.path.join(team_dir, "jinxs"), dirs_exist_ok=True)
1247
- else:
1248
- team_ctx_data["use_global_jinxs"] = True
1165
+ if not os.path.exists('.npcsh_global'):
1166
+ resp = input(f"No npc_team found in {os.getcwd()}. Create a new team here? [Y/n]: ").strip().lower()
1167
+ if resp in ("", "y", "yes"):
1168
+ team_dir = project_team_path
1169
+ os.makedirs(team_dir, exist_ok=True)
1170
+ default_forenpc_name = "forenpc"
1171
+ forenpc_directive = input(
1172
+ f"Enter a primary directive for {default_forenpc_name} (default: 'You are the forenpc of the team...'): "
1173
+ ).strip() or "You are the forenpc of the team, coordinating activities between NPCs on the team, verifying that results from NPCs are high quality and can help to adequately answer user requests."
1174
+ forenpc_model = input("Enter a model for your forenpc (default: llama3.2): ").strip() or "llama3.2"
1175
+ forenpc_provider = input("Enter a provider for your forenpc (default: ollama): ").strip() or "ollama"
1176
+
1177
+ with open(os.path.join(team_dir, f"{default_forenpc_name}.npc"), "w") as f:
1178
+ yaml.dump({
1179
+ "name": default_forenpc_name, "primary_directive": forenpc_directive,
1180
+ "model": forenpc_model, "provider": forenpc_provider
1181
+ }, f)
1182
+
1183
+ ctx_path = os.path.join(team_dir, "team.ctx")
1184
+ folder_context = input("Enter a short description for this project/team (optional): ").strip()
1185
+ team_ctx_data = {
1186
+ "forenpc": default_forenpc_name, "model": forenpc_model,
1187
+ "provider": forenpc_provider, "api_key": None, "api_url": None,
1188
+ "context": folder_context if folder_context else None
1189
+ }
1190
+ use_jinxs = input("Use global jinxs folder (g) or copy to this project (c)? [g/c, default: g]: ").strip().lower()
1191
+ if use_jinxs == "c":
1192
+ global_jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
1193
+ if os.path.exists(global_jinxs_dir):
1194
+ shutil.copytree(global_jinxs_dir, os.path.join(team_dir, "jinxs"), dirs_exist_ok=True)
1195
+ else:
1196
+ team_ctx_data["use_global_jinxs"] = True
1249
1197
 
1250
- with open(ctx_path, "w") as f:
1251
- yaml.dump(team_ctx_data, f)
1198
+ with open(ctx_path, "w") as f:
1199
+ yaml.dump(team_ctx_data, f)
1200
+ else:
1201
+ render_markdown('From now on, npcsh will assume you will use the global team when activating from this folder. \n If you change your mind and want to initialize a team, use /init from within npcsh, `npc init` or `rm .npcsh_global` from the current working directory.')
1202
+ with open(".npcsh_global", "w") as f:
1203
+ pass
1204
+ team_dir = global_team_path
1205
+ default_forenpc_name = "sibiji"
1252
1206
  elif os.path.exists(global_team_path):
1253
1207
  team_dir = global_team_path
1254
- default_forenpc_name = "sibiji"
1255
- else:
1256
- print("No global npc_team found. Please run 'npcpy init' or create a team first.")
1257
- sys.exit(1)
1208
+ default_forenpc_name = "sibiji"
1209
+
1258
1210
 
1259
1211
  team_ctx = {}
1260
1212
  for filename in os.listdir(team_dir):
@@ -1368,6 +1320,12 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1368
1320
 
1369
1321
  def exit_shell(state):
1370
1322
  print("\nGoodbye!")
1323
+ # update the team ctx file to update the context and the preferences
1324
+
1325
+
1326
+
1327
+
1328
+
1371
1329
  #print('beginning knowledge consolidation')
1372
1330
  #try:
1373
1331
  # breathe_result = breathe(state.messages, state.chat_model, state.chat_provider, state.npc)
@@ -1431,27 +1389,6 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1431
1389
  # Ctrl+D: exit shell cleanly
1432
1390
  exit_shell(state)
1433
1391
 
1434
- def run_non_interactive(command_history: CommandHistory, initial_state: ShellState):
1435
- state = initial_state
1436
- # print("Running in non-interactive mode...", file=sys.stderr) # Optional debug
1437
-
1438
- for line in sys.stdin:
1439
- user_input = line.strip()
1440
- if not user_input:
1441
- continue
1442
- if user_input.lower() in ["exit", "quit"]:
1443
- break
1444
-
1445
- state.current_path = os.getcwd()
1446
- state, output = execute_command(user_input, state)
1447
- # Non-interactive: just print raw output, don't process results complexly
1448
- if state.stream_output and isgenerator(output):
1449
- for chunk in output: print(str(chunk), end='')
1450
- print()
1451
- elif output is not None:
1452
- print(output)
1453
- # Maybe still log history?
1454
- # process_result(user_input, state, output, command_history)
1455
1392
 
1456
1393
  def main() -> None:
1457
1394
  parser = argparse.ArgumentParser(description="npcsh - An NPC-powered shell.")
@@ -1469,6 +1406,9 @@ def main() -> None:
1469
1406
  initial_state.team = team
1470
1407
  #import pdb
1471
1408
  #pdb.set_trace()
1409
+
1410
+ # add a -g global command to indicate if to use the global or project, otherwise go thru normal flow
1411
+
1472
1412
  if args.command:
1473
1413
  state = initial_state
1474
1414
  state.current_path = os.getcwd()
@@ -1478,9 +1418,6 @@ def main() -> None:
1478
1418
  print()
1479
1419
  elif output is not None:
1480
1420
  print(output)
1481
-
1482
- elif not sys.stdin.isatty():
1483
- run_non_interactive(command_history, initial_state)
1484
1421
  else:
1485
1422
  run_repl(command_history, initial_state)
1486
1423
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: npcsh is a command-line toolkit for using AI agents in novel ways.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcsh
6
6
  Author: Christopher Agostino
@@ -84,7 +84,7 @@ extra_files = package_files("npcpy/npc_team/")
84
84
 
85
85
  setup(
86
86
  name="npcsh",
87
- version="1.0.3",
87
+ version="1.0.5",
88
88
  packages=find_packages(exclude=["tests*"]),
89
89
  install_requires=base_requirements, # Only install base requirements by default
90
90
  extras_require={
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes