npcsh 1.0.23__tar.gz → 1.0.25__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.
Files changed (52) hide show
  1. {npcsh-1.0.23 → npcsh-1.0.25}/PKG-INFO +1 -1
  2. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/guac.py +206 -160
  3. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh.egg-info/PKG-INFO +1 -1
  4. {npcsh-1.0.23 → npcsh-1.0.25}/setup.py +1 -1
  5. {npcsh-1.0.23 → npcsh-1.0.25}/LICENSE +0 -0
  6. {npcsh-1.0.23 → npcsh-1.0.25}/README.md +0 -0
  7. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/__init__.py +0 -0
  8. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/_state.py +0 -0
  9. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/alicanto.py +0 -0
  10. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/corca.py +0 -0
  11. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/mcp_helpers.py +0 -0
  12. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/mcp_server.py +0 -0
  13. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc.py +0 -0
  14. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/alicanto.npc +0 -0
  15. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/alicanto.png +0 -0
  16. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/corca.npc +0 -0
  17. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/corca.png +0 -0
  18. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/foreman.npc +0 -0
  19. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/frederic.npc +0 -0
  20. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/frederic4.png +0 -0
  21. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/guac.png +0 -0
  22. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/jinxs/bash_executer.jinx +0 -0
  23. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/jinxs/edit_file.jinx +0 -0
  24. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/jinxs/image_generation.jinx +0 -0
  25. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/jinxs/internet_search.jinx +0 -0
  26. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/jinxs/python_executor.jinx +0 -0
  27. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/jinxs/screen_cap.jinx +0 -0
  28. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/kadiefa.npc +0 -0
  29. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/kadiefa.png +0 -0
  30. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/npcsh.ctx +0 -0
  31. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/npcsh_sibiji.png +0 -0
  32. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/plonk.npc +0 -0
  33. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/plonk.png +0 -0
  34. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/plonkjr.npc +0 -0
  35. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/plonkjr.png +0 -0
  36. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/sibiji.npc +0 -0
  37. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/sibiji.png +0 -0
  38. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/spool.png +0 -0
  39. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npc_team/yap.png +0 -0
  40. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/npcsh.py +0 -0
  41. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/plonk.py +0 -0
  42. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/pti.py +0 -0
  43. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/routes.py +0 -0
  44. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/spool.py +0 -0
  45. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/wander.py +0 -0
  46. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh/yap.py +0 -0
  47. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh.egg-info/SOURCES.txt +0 -0
  48. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh.egg-info/dependency_links.txt +0 -0
  49. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh.egg-info/entry_points.txt +0 -0
  50. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh.egg-info/requires.txt +0 -0
  51. {npcsh-1.0.23 → npcsh-1.0.25}/npcsh.egg-info/top_level.txt +0 -0
  52. {npcsh-1.0.23 → npcsh-1.0.25}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.23
3
+ Version: 1.0.25
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
@@ -11,7 +11,7 @@ import matplotlib.pyplot as plt
11
11
 
12
12
  import logging
13
13
  plt.ioff()
14
-
14
+ import shlex
15
15
  import platform
16
16
  import yaml
17
17
  import re
@@ -396,9 +396,13 @@ def _handle_guac_refresh(state: ShellState, project_name: str, src_dir: Path):
396
396
  prompt = "\n".join(prompt_parts)
397
397
 
398
398
  try:
399
+ # Ensure state.npc is not None before accessing .model or .provider
400
+ npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
401
+ npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
402
+
399
403
  response = get_llm_response(prompt,
400
- model=state.chat_model,
401
- provider=state.chat_provider,
404
+ model=npc_model,
405
+ provider=npc_provider,
402
406
  npc=state.npc,
403
407
  stream=False)
404
408
  suggested_code_raw = response.get("response", "").strip()
@@ -434,22 +438,69 @@ def _handle_guac_refresh(state: ShellState, project_name: str, src_dir: Path):
434
438
  except Exception as e:
435
439
  print(f"Error during /refresh: {e}")
436
440
  traceback.print_exc()
437
- def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None, lang='python', default_mode_choice=None):
441
+
442
+
443
+
444
+ def ensure_global_guac_team():
445
+ """Ensure a global guac team exists at ~/.npcsh/guac/npc_team/."""
446
+ base_dir = Path.home() / ".npcsh" / "guac"
447
+ team_dir = base_dir / "npc_team"
448
+ team_dir.mkdir(parents=True, exist_ok=True)
449
+
450
+ guac_npc_path = team_dir / "guac.npc"
451
+ if not guac_npc_path.exists():
452
+ guac = {
453
+ "name": "guac",
454
+ "primary_directive": (
455
+ "You are guac, the global coordinator NPC for Guac Mode. "
456
+ "Always prioritize Python code, concise answers, and coordination."
457
+ ),
458
+ "model": os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b"),
459
+ "provider": os.environ.get("NPCSH_CHAT_PROVIDER", "ollama")
460
+ }
461
+ with open(guac_npc_path, "w") as f:
462
+ yaml.dump(guac, f, default_flow_style=False)
463
+ print(f"✅ Created global guac NPC at {guac_npc_path}")
464
+
465
+ ctx_path = team_dir / "team.ctx"
466
+ if not ctx_path.exists():
467
+ ctx = {
468
+ "team_name": "guac_global_team",
469
+ "forenpc": "guac",
470
+ "model": os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b"),
471
+ "provider": os.environ.get("NPCSH_CHAT_PROVIDER", "ollama"),
472
+ "description": "Global guac team context"
473
+ }
474
+ with open(ctx_path, "w") as f:
475
+ yaml.dump(ctx, f, default_flow_style=False)
476
+ print(f"✅ Created global guac team.ctx at {ctx_path}")
477
+
478
+ return team_dir
479
+ def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None,
480
+ lang='python', default_mode_choice=None):
438
481
  base_dir = Path.cwd()
482
+
483
+ # Check if we should default to global without prompting
484
+ if GUAC_GLOBAL_FLAG_FILE.exists():
485
+ print("💡 Using global Guac team as default (previously set).")
486
+ team_dir = ensure_global_guac_team()
487
+ return {
488
+ "language": lang, "package_root": team_dir, "plots_dir": plots_dir,
489
+ "npc_team_dir": team_dir, "config_dir": config_dir, "default_mode": default_mode_choice or "agent",
490
+ "project_description": "Global guac team for analysis.", "package_name": "guac"
491
+ }
492
+
493
+ # default: project npc_team_dir
439
494
  if npc_team_dir is None:
440
495
  npc_team_dir = base_dir / "npc_team"
441
496
  else:
442
497
  npc_team_dir = Path(npc_team_dir)
443
498
  npc_team_dir.mkdir(parents=True, exist_ok=True)
444
- # Setup Guac workspace
445
499
  workspace_dirs = _get_workspace_dirs(npc_team_dir)
446
-
447
500
  _ensure_workspace_dirs(workspace_dirs)
448
501
 
449
- # Rest of existing setup_guac_mode code...
450
502
  team_ctx_path = npc_team_dir / "team.ctx"
451
503
  existing_ctx = {}
452
-
453
504
  if team_ctx_path.exists():
454
505
  try:
455
506
  with open(team_ctx_path, "r") as f:
@@ -459,99 +510,69 @@ def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None, lang='py
459
510
 
460
511
  package_root = existing_ctx.get("GUAC_PACKAGE_ROOT")
461
512
  package_name = existing_ctx.get("GUAC_PACKAGE_NAME")
462
-
513
+
463
514
  if package_root is None or package_name is None:
464
515
  try:
465
- response = input("Enter the path to your Python package root (press Enter for current directory): ").strip()
516
+ response = input("Enter package root (Enter for current dir): ").strip()
466
517
  package_root = response if response else str(base_dir)
467
-
468
- response = input("Enter your package name (press Enter to use 'project'): ").strip()
518
+ response = input("Enter package name (Enter for 'project'): ").strip()
469
519
  package_name = response if response else "project"
470
- except EOFError:
471
- package_root = str(base_dir)
472
- package_name = "project"
473
-
474
- project_description = existing_ctx.get("GUAC_PROJECT_DESCRIPTION")
475
-
476
- if project_description is None:
520
+ except (KeyboardInterrupt, EOFError):
521
+ print("⚠️ Project setup interrupted. Falling back to global guac team...")
522
+ GUAC_GLOBAL_FLAG_FILE.touch() # Create the flag file to remember this choice
523
+ team_dir = ensure_global_guac_team()
524
+ return {
525
+ "language": lang, "package_root": team_dir, "plots_dir": plots_dir,
526
+ "npc_team_dir": team_dir, "config_dir": config_dir, "default_mode": default_mode_choice or "agent",
527
+ "project_description": "Global guac team for analysis.", "package_name": "guac"
528
+ }
529
+
530
+ project_description = existing_ctx.get("GUAC_PROJECT_DESCRIPTION", "")
531
+ if not project_description:
477
532
  try:
478
- project_description = input("Enter a short description of the project: ").strip() or "No description provided."
479
- except EOFError:
533
+ project_description = input("Enter a project description: ").strip() or "No description."
534
+ except (KeyboardInterrupt, EOFError):
480
535
  project_description = "No description provided."
481
536
 
482
537
  updated_ctx = {**existing_ctx}
483
538
  updated_ctx.update({
484
539
  "GUAC_TEAM_NAME": "guac_team",
485
- "GUAC_DESCRIPTION": f"A team of NPCs specialized in {lang} analysis for project {package_name}",
486
- "GUAC_FORENPC": "guac",
487
- "GUAC_PROJECT_DESCRIPTION": project_description,
488
- "GUAC_LANG": lang,
489
- "GUAC_PACKAGE_ROOT": package_root,
490
- "GUAC_PACKAGE_NAME": package_name,
540
+ "GUAC_DESCRIPTION": f"A team for {lang} analysis for project {package_name}",
541
+ "GUAC_FORENPC": "guac", "GUAC_PROJECT_DESCRIPTION": project_description,
542
+ "GUAC_LANG": lang, "GUAC_PACKAGE_ROOT": package_root, "GUAC_PACKAGE_NAME": package_name,
491
543
  "GUAC_WORKSPACE_PATHS": {k: str(v) for k, v in workspace_dirs.items()},
492
544
  })
493
-
494
- pkg_root_path = Path(package_root)
495
- try:
496
- pkg_root_path.mkdir(parents=True, exist_ok=True)
497
- package_dir = pkg_root_path / package_name
498
-
499
- if not package_dir.exists():
500
- package_dir.mkdir(parents=True, exist_ok=True)
501
- (package_dir / "__init__.py").write_text("# package initialized by setup_guac_mode\n")
502
- logging.info("Created minimal package directory at %s", package_dir)
503
- except Exception as e:
504
- logging.warning("Could not ensure package root/dir: %s", e)
545
+
546
+ pkg_root_path = Path(package_root)
547
+ try:
548
+ pkg_root_path.mkdir(parents=True, exist_ok=True)
549
+ (pkg_root_path / package_name / "__init__.py").touch()
550
+ except Exception as e:
551
+ logging.warning("Could not ensure package root/dir: %s", e)
552
+
505
553
  with open(team_ctx_path, "w") as f:
506
554
  yaml.dump(updated_ctx, f, default_flow_style=False)
507
555
  print("Updated team.ctx with GUAC-specific information.")
508
556
 
509
-
510
-
511
- setup_py_path = pkg_root_path / "setup.py"
512
-
513
-
514
- try:
515
- if not setup_py_path.exists():
516
- setup_content = f'''
517
- from setuptools import setup, find_packages
518
- setup(
519
- name="{package_name}",
520
- version="{existing_ctx.get("GUAC_PACKAGE_VERSION", "0.0.0")}",
521
- description="{project_description.replace('"', '\\"')}",
522
- packages=find_packages(),
523
- include_package_data=True,
524
- install_requires=[],
525
- )
526
- '''
527
- setup_py_path.write_text(setup_content)
528
- logging.info("Created minimal setup.py at %s", setup_py_path)
529
- except Exception as e:
530
- logging.warning("Could not write setup.py: %s", e)
531
-
557
+ setup_py_path = pkg_root_path / "setup.py"
558
+ desc = project_description.replace('"', '\\"')
559
+ if not setup_py_path.exists():
560
+ setup_content = f'''from setuptools import setup, find_packages
561
+ setup(name="{package_name}", version="0.0.1", description="{desc}", packages=find_packages())
562
+ '''
563
+ setup_py_path.write_text(setup_content)
564
+ logging.info("Created minimal setup.py at %s", setup_py_path)
565
+
532
566
  default_mode_val = default_mode_choice or "agent"
533
567
  setup_npc_team(npc_team_dir, lang)
534
-
535
-
536
568
 
537
569
  print(f"\nGuac mode configured for package: {package_name} at {package_root}")
538
570
  print(f"Workspace created at: {workspace_dirs['workspace']}")
539
-
540
571
  return {
541
- "language": lang,
542
- "package_root": Path(package_root),
543
- "plots_dir": plots_dir,
544
- "npc_team_dir": npc_team_dir,
545
- "config_dir": config_dir,
546
- "default_mode": default_mode_val,
547
- "project_description": project_description,
548
- "package_name": package_name
572
+ "language": lang, "package_root": Path(package_root), "plots_dir": plots_dir,
573
+ "npc_team_dir": npc_team_dir, "config_dir": config_dir, "default_mode": default_mode_val,
574
+ "project_description": project_description, "package_name": package_name
549
575
  }
550
-
551
-
552
-
553
-
554
-
555
576
  def setup_npc_team(npc_team_dir, lang, is_subteam=False):
556
577
  # Create Guac-specific NPCs
557
578
  guac_npc = {
@@ -566,7 +587,7 @@ def setup_npc_team(npc_team_dir, lang, is_subteam=False):
566
587
  }
567
588
  caug_npc = {
568
589
  "name": "caug",
569
- "primary_directive": f"You are caug, a specialist in big data statistical methods in {lang}."
590
+ "primary_directive": f"You are caug, a specialist in big data statistical methods in {lang}. You never make scatter plots with discrete values unless asked. "
570
591
  }
571
592
 
572
593
  parsely_npc = {
@@ -1005,7 +1026,7 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
1005
1026
  data_summary=f"{data_points} data points",
1006
1027
  change_significance=1.0 if not last else 0.5
1007
1028
  )
1008
-
1029
+
1009
1030
  session.add(plot_state)
1010
1031
  session.commit()
1011
1032
  session.close()
@@ -1122,17 +1143,42 @@ import sys
1122
1143
  from io import StringIO
1123
1144
  from contextlib import redirect_stdout, redirect_stderr
1124
1145
 
1146
+ # --- New Emoji Function for Agentic Mode ---
1147
+ def _get_guac_agent_emoji(failures: int, max_fail: int = 3) -> str:
1148
+ """
1149
+ Returns an avocado emoji representing the state based on consecutive failures.
1150
+ Includes "puke" emoji for max_fail, and "skull" for exceeding max_fail + 20.
1151
+ """
1152
+ if failures == 0:
1153
+ return "🥑" # Fresh
1154
+ elif failures == 1:
1155
+ return "🥑🔪" # Sliced, contemplating next steps
1156
+ elif failures == 2:
1157
+ return "🥑🥣" # In the bowl, getting mashed
1158
+ elif failures == max_fail:
1159
+ return "🥑🤢" # Going bad, critical issue (puke)
1160
+ elif failures > max_fail + 20: # Skull for 20+ over max
1161
+ return "🥑💀" # Rotten
1162
+ elif failures > max_fail:
1163
+ return "🥑🟤" # Bruised and bad
1164
+ else:
1165
+ return "🥑❓" # Unknown state
1166
+
1167
+ # --- New Helper for Persisting Global Choice ---
1168
+ GUAC_GLOBAL_FLAG_FILE = Path.home() / ".npcsh" / ".guac_use_global"
1169
+
1170
+
1125
1171
  def _run_agentic_mode(command: str,
1126
1172
  state: ShellState,
1127
1173
  locals_dict: Dict[str, Any],
1128
1174
  npc_team_dir: Path) -> Tuple[ShellState, Any]:
1129
1175
  """Run agentic mode with continuous iteration based on progress"""
1130
- max_iterations = 3 # low maximum as a safety limit
1176
+ max_iterations = 5 # Increased slightly for more complex tasks
1131
1177
  iteration = 0
1132
1178
  full_output = []
1133
1179
  current_command = command
1134
1180
  consecutive_failures = 0
1135
- max_consecutive_failures = 3
1181
+ max_consecutive_failures = 3 # This is the limit before stopping
1136
1182
 
1137
1183
  # Build context of existing variables
1138
1184
  existing_vars_context = "EXISTING VARIABLES IN ENVIRONMENT:\n"
@@ -1151,7 +1197,7 @@ def _run_agentic_mode(command: str,
1151
1197
  steps = []
1152
1198
  while iteration < max_iterations and consecutive_failures < max_consecutive_failures:
1153
1199
  iteration += 1
1154
- print(f"\n🔄 Agentic iteration {iteration}")
1200
+ print(f"\n{_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)} Agentic iteration {iteration} ")
1155
1201
 
1156
1202
  prompt = f"""
1157
1203
  USER REQUEST: {current_command} {next_step}
@@ -1202,12 +1248,13 @@ def _run_agentic_mode(command: str,
1202
1248
  If a user is asking for help debugging, it's better to figure out what is wrong by attempting to run it yourself, and if they do not prefer that,
1203
1249
  then it's best to use static parsing methods and arguments based on deduction rather than attempting to just fix everything over and over.
1204
1250
 
1205
- Do not over-complicate the code.
1251
+ Do not over- complicate the code.
1206
1252
 
1207
1253
  Do not include any '__name__'=='__main__' block.
1208
1254
  """
1209
- #
1210
1255
 
1256
+ npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
1257
+ npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
1211
1258
 
1212
1259
  llm_response = get_llm_response(prompt,
1213
1260
  npc=state.npc,
@@ -1215,21 +1262,18 @@ def _run_agentic_mode(command: str,
1215
1262
  messages=state.messages)
1216
1263
 
1217
1264
  generated_code = print_and_process_stream(llm_response.get('response'),
1218
- state.npc.model,
1219
- state.npc.provider
1265
+ npc_model,
1266
+ npc_provider
1220
1267
  )
1221
1268
 
1222
-
1223
1269
  state.messages.append({'role':'user', 'content':current_command })
1224
1270
  state.messages.append({'role':'assistant', 'content': generated_code})
1225
1271
 
1226
1272
  if '<request_for_input>' in generated_code:
1227
1273
  generated_code = generated_code.split('>')[1].split('<')[0]
1228
1274
  user_feedback = input("\n🤔 Agent requests feedback (press Enter to continue or type your input): ").strip()
1229
-
1230
1275
  current_command = f"{current_command} - User feedback: {user_feedback}"
1231
1276
  max_iterations += int(max_iterations/2)
1232
-
1233
1277
  continue
1234
1278
 
1235
1279
  if generated_code.startswith('```python'):
@@ -1237,13 +1281,9 @@ def _run_agentic_mode(command: str,
1237
1281
  if generated_code.endswith('```'):
1238
1282
  generated_code = generated_code[:-3].strip()
1239
1283
 
1240
- #print(f"\n# Generated Code (Iteration {iteration}):\n---\n{generated_code}\n---\n")
1241
-
1242
1284
  try:
1243
- # Capture stdout/stderr during execution
1244
1285
  stdout_capture = StringIO()
1245
1286
  stderr_capture = StringIO()
1246
-
1247
1287
  with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
1248
1288
  state, exec_output = execute_python_code(generated_code,
1249
1289
  state,
@@ -1251,97 +1291,73 @@ def _run_agentic_mode(command: str,
1251
1291
 
1252
1292
  captured_stdout = stdout_capture.getvalue()
1253
1293
  captured_stderr = stderr_capture.getvalue()
1254
- print(exec_output)
1255
-
1256
- if captured_stdout:
1294
+ if exec_output: print(exec_output)
1295
+ if captured_stdout:
1257
1296
  print("\n📤 Captured stdout:\n", captured_stdout)
1258
- if captured_stderr:
1297
+ if captured_stderr:
1259
1298
  print("\n❌ Captured stderr:\n", captured_stderr)
1260
1299
 
1261
1300
  combined_output = f"{exec_output}\nstdout:\n{captured_stdout}\nstderr:\n{captured_stderr}"
1262
1301
  full_output.append(f"Iteration {iteration}:\nCode:\n{generated_code}\nOutput:\n{combined_output}")
1263
1302
 
1264
- # Update the context with new variables
1265
1303
  new_vars = []
1266
1304
  for var_name, var_value in locals_dict.items():
1267
1305
  if (not var_name.startswith('_') and
1268
1306
  var_name not in existing_vars_context and
1269
1307
  var_name not in ['In', 'Out', 'exit', 'quit', 'get_ipython']):
1270
1308
  new_vars.append(var_name)
1271
-
1272
1309
  if new_vars:
1273
1310
  existing_vars_context += f"\nNEW VARIABLES CREATED: {', '.join(new_vars)}\n"
1274
1311
 
1275
1312
  analysis_prompt = f"""
1276
1313
  CODE EXECUTION RESULTS: {combined_output}
1277
-
1278
1314
  EXISTING VARIABLES: {existing_vars_context}
1279
-
1280
1315
  EXECUTED_CODE: {generated_code}
1281
-
1282
1316
  PREVIOUS_CODE: {previous_code}
1283
-
1284
1317
  PREVIOUS ATTEMPTS: ```{full_output[-3:] if full_output else 'None'}```
1285
-
1286
1318
  Here are the steps so far: {steps}
1287
-
1288
1319
  ANALYSIS:
1289
- - Is there MEANINGFUL PROGRESS? Return 'progress' if making good progress. If the previous code and current executed code are essentially accomplishing the same thing, that is not progress. If the steps have been too similar or not improved, then consider it a problem.
1290
- - Is there a PROBLEM? Return 'problem' if stuck or error occurred
1291
- - Is there an ambiguity that should be resolved? Return 'question'.
1292
- - Is the analysis complete enough to get feedback from the user? If it's pretty much done, return 'complete'
1293
- - Return ONLY one of these words followed by a brief explanation to take the next step forward.
1320
+ - Is there MEANINGFUL PROGRESS? Return 'progress' if making good progress.
1321
+ - Is there a PROBLEM? Return 'problem' if stuck or error occurred.
1322
+ - Is there an AMBIGUITY that should be resolved? Return 'question'.
1323
+ - Is the analysis COMPLETE enough to get feedback? If it's pretty much done, return 'complete'.
1324
+ - Return ONLY one of these words followed by a brief explanation for the next step.
1294
1325
  """
1295
- analysis_response = get_llm_response(analysis_prompt,
1296
- npc=state.npc,
1297
- stream=False)
1298
-
1326
+ analysis_response = get_llm_response(analysis_prompt, npc=state.npc, stream=False)
1299
1327
  analysis = analysis_response.get("response", "").strip().lower()
1300
1328
  next_step = analysis[8:]
1301
- state.messages.append({'role':'assistant', 'content':f'''- Is there MEANINGFUL PROGRESS? Is there a PROBLEM? Is there an ambiguity that should be resolved?
1302
- Indeed: {analysis} '''})
1303
- print(f"\n# Analysis:\n{analysis}")
1304
-
1305
- previous_code = generated_code
1329
+ state.messages.append({'role':'assistant',
1330
+ 'content':f'Is there progress? is there a problem/ is there ambiguity? is it complete?\n {analysis}'})
1306
1331
 
1307
1332
  if analysis.startswith('complete'):
1308
- print("✅ Task completed successfully!")
1333
+ print(f"✅ Task completed! {_get_guac_agent_emoji(0, max_consecutive_failures)}")
1309
1334
  break
1310
- if analysis.startswith('question'):
1311
- print('Please help answer')
1335
+ elif analysis.startswith('question'):
1336
+ print(f"🤔 Agent has a question: {next_step} {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1312
1337
  break
1313
1338
  elif analysis.startswith('progress'):
1314
- consecutive_failures = 0 # Reset failure counter on progress
1315
- print("➡️ Making progress, continuing to next iteration...")
1339
+ consecutive_failures = 0
1340
+ print(f"➡️ Making progress... {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1316
1341
  current_command = next_step
1317
1342
  elif analysis.startswith('problem'):
1318
-
1319
- print(f"⚠️ Problem detected ({consecutive_failures}/{max_consecutive_failures} consecutive failures)")
1320
-
1321
- current_command = f"{current_command} - PROBLEM in addressing it: {analysis}"
1322
- max_iterations += int(max_iterations/2)
1323
- continue
1343
+ consecutive_failures += 1
1344
+ print(f"⚠️ Problem detected ({consecutive_failures}/{max_consecutive_failures}) {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1345
+ current_command = f"{current_command} - PROBLEM: {analysis}"
1324
1346
  else:
1325
- # Default behavior for unexpected responses
1326
1347
  consecutive_failures += 1
1327
- print(f"❓ Unexpected analysis response, counting as failure ({consecutive_failures}/{max_consecutive_failures})")
1328
- if consecutive_failures >= max_consecutive_failures:
1329
- print("❌ Too many consecutive failures, stopping iteration.")
1330
- break
1331
- except KeyboardInterrupt as e:
1348
+ print(f"❓ Unexpected analysis, counting as failure ({consecutive_failures}/{max_consecutive_failures}) {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1349
+ except KeyboardInterrupt:
1332
1350
  user_input = input('User input: ')
1333
- current_command = current_command+user_input
1351
+ current_command += user_input
1334
1352
  except Exception as e:
1335
- error_msg = f"Error in iteration {iteration}: {str(e)}"
1353
+ consecutive_failures += 1
1354
+ error_msg = f"Error in iteration {iteration}: {str(e)} {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}"
1336
1355
  print(error_msg)
1337
1356
  full_output.append(error_msg)
1338
- consecutive_failures += 1
1339
1357
  current_command = f"{current_command} - Error: {str(e)}"
1340
1358
 
1341
-
1342
- if consecutive_failures >= max_consecutive_failures:
1343
- print("❌ Too many consecutive errors, stopping iteration.")
1344
- break
1359
+ if consecutive_failures >= max_consecutive_failures:
1360
+ print(f"❌ Too many consecutive failures, stopping. {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1345
1361
 
1346
1362
  return state, "# Agentic execution completed\n" + '\n'.join(full_output)
1347
1363
 
@@ -1371,14 +1387,17 @@ def get_guac_prompt_char(command_count: int, guac_refresh_period = 100) -> str:
1371
1387
  def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str, Any], project_name: str, src_dir: Path, router) -> Tuple[ShellState, Any]:
1372
1388
  stripped_command = command.strip()
1373
1389
  output = None
1374
-
1390
+ cmd_parts = shlex.split(stripped_command)
1391
+ if cmd_parts and cmd_parts[0] in ["cd", "ls", "pwd"]:
1392
+ return execute_command(stripped_command, state, review=False, router=router)
1393
+
1394
+ npc_team_dir = Path(state.team.team_path) if state.team and hasattr(state.team, 'team_path') else Path.cwd() / "npc_team"
1395
+
1375
1396
  if not stripped_command:
1376
1397
  return state, None
1377
1398
  if stripped_command.lower() in ["exit", "quit", "exit()", "quit()"]:
1378
1399
  raise SystemExit("Exiting Guac Mode.")
1379
1400
 
1380
- # Get npc_team_dir from current working directory
1381
- npc_team_dir = Path.cwd() / "npc_team"
1382
1401
  if stripped_command.startswith('run '):
1383
1402
  file_path = stripped_command[4:].strip()
1384
1403
  try:
@@ -1497,18 +1516,24 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
1497
1516
  {locals_context_string}
1498
1517
  Begin directly with the code
1499
1518
  """
1519
+ # Ensure state.npc is not None before accessing .model or .provider
1520
+ npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
1521
+ npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
1522
+
1500
1523
  llm_response = get_llm_response(prompt_cmd,
1501
- model=state.chat_model,
1502
- provider=state.chat_provider,
1503
1524
  npc=state.npc,
1504
1525
  stream=True,
1505
1526
  messages=state.messages)
1506
-
1507
- if llm_response.get('response', '').startswith('```python'):
1508
- generated_code = llm_response.get("response", "").strip()[len('```python'):].strip()
1527
+ response = print_and_process_stream(llm_response.get('response'),
1528
+ npc_model,
1529
+ npc_provider )
1530
+
1531
+
1532
+ if response.startswith('```python'):
1533
+ generated_code = response.strip()[len('```python'):].strip()
1509
1534
  generated_code = generated_code.rsplit('```', 1)[0].strip()
1510
1535
  else:
1511
- generated_code = llm_response.get("response", "").strip()
1536
+ generated_code = response.strip()
1512
1537
 
1513
1538
  state.messages = llm_response.get("messages", state.messages)
1514
1539
 
@@ -1776,6 +1801,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
1776
1801
  state.current_path = os.getcwd()
1777
1802
 
1778
1803
  display_model = state.chat_model
1804
+ # Ensure state.npc is not None before accessing .model or .provider
1779
1805
  if isinstance(state.npc, NPC) and state.npc.model:
1780
1806
  display_model = state.npc.model
1781
1807
 
@@ -1860,12 +1886,34 @@ def enter_guac_mode(npc=None,
1860
1886
  default_mode_choice=default_mode_choice
1861
1887
  )
1862
1888
 
1863
- project_name = setup_result.get("project_name", "project")
1889
+ project_name = setup_result.get("package_name", "project")
1864
1890
  package_root = setup_result["package_root"]
1865
1891
  package_name = setup_result.get("package_name", "project")
1892
+ npc_team_dir = setup_result.get("npc_team_dir")
1866
1893
 
1894
+ # Always call setup_shell to build history, team, and default_npc
1867
1895
  command_history, default_team, default_npc = setup_shell()
1868
-
1896
+
1897
+ # 🔑 Ensure global guac gets loaded if npc is None
1898
+ if npc is None and default_npc is None:
1899
+ # Construct the path correctly based on where ensure_global_guac_team puts it
1900
+ guac_npc_path = Path(npc_team_dir) / "guac.npc"
1901
+ if guac_npc_path.exists():
1902
+ npc = NPC(file=str(guac_npc_path), db_conn=command_history.engine)
1903
+ # Ensure the team is also correctly set to the global guac team
1904
+ team_ctx_path = Path(npc_team_dir) / "team.ctx"
1905
+ if team_ctx_path.exists():
1906
+ with open(team_ctx_path, "r") as f:
1907
+ team_ctx = yaml.safe_load(f) or {}
1908
+ team = Team(team_path=str(npc_team_dir), forenpc=npc, jinxs={}) # Simplified team creation
1909
+ team.name = team_ctx.get("team_name", "guac_global_team")
1910
+ else:
1911
+ raise RuntimeError(f"No NPC loaded and {guac_npc_path} not found!")
1912
+ elif default_npc and npc is None:
1913
+ # If setup_shell provided a default_npc (e.g., sibiji), ensure it's used
1914
+ npc = default_npc
1915
+
1916
+
1869
1917
  state = ShellState(
1870
1918
  conversation_id=start_new_conversation(),
1871
1919
  stream_output=True,
@@ -1873,8 +1921,8 @@ def enter_guac_mode(npc=None,
1873
1921
  chat_model=os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b"),
1874
1922
  chat_provider=os.environ.get("NPCSH_CHAT_PROVIDER", "ollama"),
1875
1923
  current_path=os.getcwd(),
1876
- npc=npc or default_npc,
1877
- team=team or default_team
1924
+ npc=npc, # This is now guaranteed to be an NPC object
1925
+ team=team or default_team # Use the correctly loaded team or default
1878
1926
  )
1879
1927
 
1880
1928
  state.command_history = command_history
@@ -1889,8 +1937,6 @@ def enter_guac_mode(npc=None,
1889
1937
  print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
1890
1938
 
1891
1939
  run_guac_repl(state, project_name, package_root, package_name)
1892
-
1893
-
1894
1940
 
1895
1941
  def main():
1896
1942
  parser = argparse.ArgumentParser(description="Enter Guac Mode - Interactive Python with LLM assistance.")
@@ -1913,4 +1959,4 @@ def main():
1913
1959
  )
1914
1960
 
1915
1961
  if __name__ == "__main__":
1916
- main()
1962
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.23
3
+ Version: 1.0.25
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
@@ -78,7 +78,7 @@ extra_files = package_files("npcsh/npc_team/")
78
78
 
79
79
  setup(
80
80
  name="npcsh",
81
- version="1.0.23",
81
+ version="1.0.25",
82
82
  packages=find_packages(exclude=["tests*"]),
83
83
  install_requires=base_requirements, # Only install base requirements by default
84
84
  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
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