npcsh 1.0.22__tar.gz → 1.0.24__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.22 → npcsh-1.0.24}/PKG-INFO +1 -1
  2. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/guac.py +205 -160
  3. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/routes.py +1 -1
  4. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh.egg-info/PKG-INFO +1 -1
  5. {npcsh-1.0.22 → npcsh-1.0.24}/setup.py +1 -1
  6. {npcsh-1.0.22 → npcsh-1.0.24}/LICENSE +0 -0
  7. {npcsh-1.0.22 → npcsh-1.0.24}/README.md +0 -0
  8. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/__init__.py +0 -0
  9. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/_state.py +0 -0
  10. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/alicanto.py +0 -0
  11. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/corca.py +0 -0
  12. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/mcp_helpers.py +0 -0
  13. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/mcp_server.py +0 -0
  14. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc.py +0 -0
  15. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/alicanto.npc +0 -0
  16. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/alicanto.png +0 -0
  17. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/corca.npc +0 -0
  18. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/corca.png +0 -0
  19. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/foreman.npc +0 -0
  20. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/frederic.npc +0 -0
  21. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/frederic4.png +0 -0
  22. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/guac.png +0 -0
  23. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/jinxs/bash_executer.jinx +0 -0
  24. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/jinxs/edit_file.jinx +0 -0
  25. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/jinxs/image_generation.jinx +0 -0
  26. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/jinxs/internet_search.jinx +0 -0
  27. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/jinxs/python_executor.jinx +0 -0
  28. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/jinxs/screen_cap.jinx +0 -0
  29. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/kadiefa.npc +0 -0
  30. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/kadiefa.png +0 -0
  31. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/npcsh.ctx +0 -0
  32. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/npcsh_sibiji.png +0 -0
  33. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/plonk.npc +0 -0
  34. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/plonk.png +0 -0
  35. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/plonkjr.npc +0 -0
  36. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/plonkjr.png +0 -0
  37. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/sibiji.npc +0 -0
  38. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/sibiji.png +0 -0
  39. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/spool.png +0 -0
  40. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npc_team/yap.png +0 -0
  41. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/npcsh.py +0 -0
  42. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/plonk.py +0 -0
  43. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/pti.py +0 -0
  44. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/spool.py +0 -0
  45. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/wander.py +0 -0
  46. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh/yap.py +0 -0
  47. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh.egg-info/SOURCES.txt +0 -0
  48. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh.egg-info/dependency_links.txt +0 -0
  49. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh.egg-info/entry_points.txt +0 -0
  50. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh.egg-info/requires.txt +0 -0
  51. {npcsh-1.0.22 → npcsh-1.0.24}/npcsh.egg-info/top_level.txt +0 -0
  52. {npcsh-1.0.22 → npcsh-1.0.24}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.22
3
+ Version: 1.0.24
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,68 @@ 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
+ if not setup_py_path.exists():
559
+ setup_content = f'''from setuptools import setup, find_packages
560
+ setup(name="{package_name}", version="0.0.1", description="{project_description.replace('"', '\\"')}", packages=find_packages())
561
+ '''
562
+ setup_py_path.write_text(setup_content)
563
+ logging.info("Created minimal setup.py at %s", setup_py_path)
564
+
532
565
  default_mode_val = default_mode_choice or "agent"
533
566
  setup_npc_team(npc_team_dir, lang)
534
-
535
-
536
567
 
537
568
  print(f"\nGuac mode configured for package: {package_name} at {package_root}")
538
569
  print(f"Workspace created at: {workspace_dirs['workspace']}")
539
-
540
570
  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
571
+ "language": lang, "package_root": Path(package_root), "plots_dir": plots_dir,
572
+ "npc_team_dir": npc_team_dir, "config_dir": config_dir, "default_mode": default_mode_val,
573
+ "project_description": project_description, "package_name": package_name
549
574
  }
550
-
551
-
552
-
553
-
554
-
555
575
  def setup_npc_team(npc_team_dir, lang, is_subteam=False):
556
576
  # Create Guac-specific NPCs
557
577
  guac_npc = {
@@ -566,7 +586,7 @@ def setup_npc_team(npc_team_dir, lang, is_subteam=False):
566
586
  }
567
587
  caug_npc = {
568
588
  "name": "caug",
569
- "primary_directive": f"You are caug, a specialist in big data statistical methods in {lang}."
589
+ "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
590
  }
571
591
 
572
592
  parsely_npc = {
@@ -1005,7 +1025,7 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
1005
1025
  data_summary=f"{data_points} data points",
1006
1026
  change_significance=1.0 if not last else 0.5
1007
1027
  )
1008
-
1028
+
1009
1029
  session.add(plot_state)
1010
1030
  session.commit()
1011
1031
  session.close()
@@ -1122,17 +1142,42 @@ import sys
1122
1142
  from io import StringIO
1123
1143
  from contextlib import redirect_stdout, redirect_stderr
1124
1144
 
1145
+ # --- New Emoji Function for Agentic Mode ---
1146
+ def _get_guac_agent_emoji(failures: int, max_fail: int = 3) -> str:
1147
+ """
1148
+ Returns an avocado emoji representing the state based on consecutive failures.
1149
+ Includes "puke" emoji for max_fail, and "skull" for exceeding max_fail + 20.
1150
+ """
1151
+ if failures == 0:
1152
+ return "🥑" # Fresh
1153
+ elif failures == 1:
1154
+ return "🥑🔪" # Sliced, contemplating next steps
1155
+ elif failures == 2:
1156
+ return "🥑🥣" # In the bowl, getting mashed
1157
+ elif failures == max_fail:
1158
+ return "🥑🤢" # Going bad, critical issue (puke)
1159
+ elif failures > max_fail + 20: # Skull for 20+ over max
1160
+ return "🥑💀" # Rotten
1161
+ elif failures > max_fail:
1162
+ return "🥑🟤" # Bruised and bad
1163
+ else:
1164
+ return "🥑❓" # Unknown state
1165
+
1166
+ # --- New Helper for Persisting Global Choice ---
1167
+ GUAC_GLOBAL_FLAG_FILE = Path.home() / ".npcsh" / ".guac_use_global"
1168
+
1169
+
1125
1170
  def _run_agentic_mode(command: str,
1126
1171
  state: ShellState,
1127
1172
  locals_dict: Dict[str, Any],
1128
1173
  npc_team_dir: Path) -> Tuple[ShellState, Any]:
1129
1174
  """Run agentic mode with continuous iteration based on progress"""
1130
- max_iterations = 3 # low maximum as a safety limit
1175
+ max_iterations = 5 # Increased slightly for more complex tasks
1131
1176
  iteration = 0
1132
1177
  full_output = []
1133
1178
  current_command = command
1134
1179
  consecutive_failures = 0
1135
- max_consecutive_failures = 3
1180
+ max_consecutive_failures = 3 # This is the limit before stopping
1136
1181
 
1137
1182
  # Build context of existing variables
1138
1183
  existing_vars_context = "EXISTING VARIABLES IN ENVIRONMENT:\n"
@@ -1151,7 +1196,7 @@ def _run_agentic_mode(command: str,
1151
1196
  steps = []
1152
1197
  while iteration < max_iterations and consecutive_failures < max_consecutive_failures:
1153
1198
  iteration += 1
1154
- print(f"\n🔄 Agentic iteration {iteration}")
1199
+ print(f"\n{_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)} Agentic iteration {iteration} ")
1155
1200
 
1156
1201
  prompt = f"""
1157
1202
  USER REQUEST: {current_command} {next_step}
@@ -1202,12 +1247,13 @@ def _run_agentic_mode(command: str,
1202
1247
  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
1248
  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
1249
 
1205
- Do not over-complicate the code.
1250
+ Do not over- complicate the code.
1206
1251
 
1207
1252
  Do not include any '__name__'=='__main__' block.
1208
1253
  """
1209
- #
1210
1254
 
1255
+ npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
1256
+ npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
1211
1257
 
1212
1258
  llm_response = get_llm_response(prompt,
1213
1259
  npc=state.npc,
@@ -1215,21 +1261,18 @@ def _run_agentic_mode(command: str,
1215
1261
  messages=state.messages)
1216
1262
 
1217
1263
  generated_code = print_and_process_stream(llm_response.get('response'),
1218
- state.npc.model,
1219
- state.npc.provider
1264
+ npc_model,
1265
+ npc_provider
1220
1266
  )
1221
1267
 
1222
-
1223
1268
  state.messages.append({'role':'user', 'content':current_command })
1224
1269
  state.messages.append({'role':'assistant', 'content': generated_code})
1225
1270
 
1226
1271
  if '<request_for_input>' in generated_code:
1227
1272
  generated_code = generated_code.split('>')[1].split('<')[0]
1228
1273
  user_feedback = input("\n🤔 Agent requests feedback (press Enter to continue or type your input): ").strip()
1229
-
1230
1274
  current_command = f"{current_command} - User feedback: {user_feedback}"
1231
1275
  max_iterations += int(max_iterations/2)
1232
-
1233
1276
  continue
1234
1277
 
1235
1278
  if generated_code.startswith('```python'):
@@ -1237,13 +1280,9 @@ def _run_agentic_mode(command: str,
1237
1280
  if generated_code.endswith('```'):
1238
1281
  generated_code = generated_code[:-3].strip()
1239
1282
 
1240
- #print(f"\n# Generated Code (Iteration {iteration}):\n---\n{generated_code}\n---\n")
1241
-
1242
1283
  try:
1243
- # Capture stdout/stderr during execution
1244
1284
  stdout_capture = StringIO()
1245
1285
  stderr_capture = StringIO()
1246
-
1247
1286
  with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
1248
1287
  state, exec_output = execute_python_code(generated_code,
1249
1288
  state,
@@ -1251,97 +1290,73 @@ def _run_agentic_mode(command: str,
1251
1290
 
1252
1291
  captured_stdout = stdout_capture.getvalue()
1253
1292
  captured_stderr = stderr_capture.getvalue()
1254
- print(exec_output)
1255
-
1256
- if captured_stdout:
1293
+ if exec_output: print(exec_output)
1294
+ if captured_stdout:
1257
1295
  print("\n📤 Captured stdout:\n", captured_stdout)
1258
- if captured_stderr:
1296
+ if captured_stderr:
1259
1297
  print("\n❌ Captured stderr:\n", captured_stderr)
1260
1298
 
1261
1299
  combined_output = f"{exec_output}\nstdout:\n{captured_stdout}\nstderr:\n{captured_stderr}"
1262
1300
  full_output.append(f"Iteration {iteration}:\nCode:\n{generated_code}\nOutput:\n{combined_output}")
1263
1301
 
1264
- # Update the context with new variables
1265
1302
  new_vars = []
1266
1303
  for var_name, var_value in locals_dict.items():
1267
1304
  if (not var_name.startswith('_') and
1268
1305
  var_name not in existing_vars_context and
1269
1306
  var_name not in ['In', 'Out', 'exit', 'quit', 'get_ipython']):
1270
1307
  new_vars.append(var_name)
1271
-
1272
1308
  if new_vars:
1273
1309
  existing_vars_context += f"\nNEW VARIABLES CREATED: {', '.join(new_vars)}\n"
1274
1310
 
1275
1311
  analysis_prompt = f"""
1276
1312
  CODE EXECUTION RESULTS: {combined_output}
1277
-
1278
1313
  EXISTING VARIABLES: {existing_vars_context}
1279
-
1280
1314
  EXECUTED_CODE: {generated_code}
1281
-
1282
1315
  PREVIOUS_CODE: {previous_code}
1283
-
1284
1316
  PREVIOUS ATTEMPTS: ```{full_output[-3:] if full_output else 'None'}```
1285
-
1286
1317
  Here are the steps so far: {steps}
1287
-
1288
1318
  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.
1319
+ - Is there MEANINGFUL PROGRESS? Return 'progress' if making good progress.
1320
+ - Is there a PROBLEM? Return 'problem' if stuck or error occurred.
1321
+ - Is there an AMBIGUITY that should be resolved? Return 'question'.
1322
+ - Is the analysis COMPLETE enough to get feedback? If it's pretty much done, return 'complete'.
1323
+ - Return ONLY one of these words followed by a brief explanation for the next step.
1294
1324
  """
1295
- analysis_response = get_llm_response(analysis_prompt,
1296
- npc=state.npc,
1297
- stream=False)
1298
-
1325
+ analysis_response = get_llm_response(analysis_prompt, npc=state.npc, stream=False)
1299
1326
  analysis = analysis_response.get("response", "").strip().lower()
1300
1327
  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
1328
+ state.messages.append({'role':'assistant',
1329
+ 'content':f'Is there progress? is there a problem/ is there ambiguity? is it complete?\n {analysis}'})
1306
1330
 
1307
1331
  if analysis.startswith('complete'):
1308
- print("✅ Task completed successfully!")
1332
+ print(f"✅ Task completed! {_get_guac_agent_emoji(0, max_consecutive_failures)}")
1309
1333
  break
1310
- if analysis.startswith('question'):
1311
- print('Please help answer')
1334
+ elif analysis.startswith('question'):
1335
+ print(f"🤔 Agent has a question: {next_step} {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1312
1336
  break
1313
1337
  elif analysis.startswith('progress'):
1314
- consecutive_failures = 0 # Reset failure counter on progress
1315
- print("➡️ Making progress, continuing to next iteration...")
1338
+ consecutive_failures = 0
1339
+ print(f"➡️ Making progress... {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1316
1340
  current_command = next_step
1317
1341
  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
1342
+ consecutive_failures += 1
1343
+ print(f"⚠️ Problem detected ({consecutive_failures}/{max_consecutive_failures}) {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1344
+ current_command = f"{current_command} - PROBLEM: {analysis}"
1324
1345
  else:
1325
- # Default behavior for unexpected responses
1326
1346
  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:
1347
+ print(f"❓ Unexpected analysis, counting as failure ({consecutive_failures}/{max_consecutive_failures}) {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1348
+ except KeyboardInterrupt:
1332
1349
  user_input = input('User input: ')
1333
- current_command = current_command+user_input
1350
+ current_command += user_input
1334
1351
  except Exception as e:
1335
- error_msg = f"Error in iteration {iteration}: {str(e)}"
1352
+ consecutive_failures += 1
1353
+ error_msg = f"Error in iteration {iteration}: {str(e)} {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}"
1336
1354
  print(error_msg)
1337
1355
  full_output.append(error_msg)
1338
- consecutive_failures += 1
1339
1356
  current_command = f"{current_command} - Error: {str(e)}"
1340
1357
 
1341
-
1342
- if consecutive_failures >= max_consecutive_failures:
1343
- print("❌ Too many consecutive errors, stopping iteration.")
1344
- break
1358
+ if consecutive_failures >= max_consecutive_failures:
1359
+ print(f"❌ Too many consecutive failures, stopping. {_get_guac_agent_emoji(consecutive_failures, max_consecutive_failures)}")
1345
1360
 
1346
1361
  return state, "# Agentic execution completed\n" + '\n'.join(full_output)
1347
1362
 
@@ -1371,14 +1386,17 @@ def get_guac_prompt_char(command_count: int, guac_refresh_period = 100) -> str:
1371
1386
  def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str, Any], project_name: str, src_dir: Path, router) -> Tuple[ShellState, Any]:
1372
1387
  stripped_command = command.strip()
1373
1388
  output = None
1374
-
1389
+ cmd_parts = shlex.split(stripped_command)
1390
+ if cmd_parts and cmd_parts[0] in ["cd", "ls", "pwd"]:
1391
+ return execute_command(stripped_command, state, review=False, router=router)
1392
+
1393
+ npc_team_dir = Path(state.team.team_path) if state.team and hasattr(state.team, 'team_path') else Path.cwd() / "npc_team"
1394
+
1375
1395
  if not stripped_command:
1376
1396
  return state, None
1377
1397
  if stripped_command.lower() in ["exit", "quit", "exit()", "quit()"]:
1378
1398
  raise SystemExit("Exiting Guac Mode.")
1379
1399
 
1380
- # Get npc_team_dir from current working directory
1381
- npc_team_dir = Path.cwd() / "npc_team"
1382
1400
  if stripped_command.startswith('run '):
1383
1401
  file_path = stripped_command[4:].strip()
1384
1402
  try:
@@ -1497,18 +1515,24 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
1497
1515
  {locals_context_string}
1498
1516
  Begin directly with the code
1499
1517
  """
1518
+ # Ensure state.npc is not None before accessing .model or .provider
1519
+ npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
1520
+ npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
1521
+
1500
1522
  llm_response = get_llm_response(prompt_cmd,
1501
- model=state.chat_model,
1502
- provider=state.chat_provider,
1503
1523
  npc=state.npc,
1504
1524
  stream=True,
1505
1525
  messages=state.messages)
1506
-
1507
- if llm_response.get('response', '').startswith('```python'):
1508
- generated_code = llm_response.get("response", "").strip()[len('```python'):].strip()
1526
+ response = print_and_process_stream(llm_response.get('response'),
1527
+ npc_model,
1528
+ npc_provider )
1529
+
1530
+
1531
+ if response.startswith('```python'):
1532
+ generated_code = response.strip()[len('```python'):].strip()
1509
1533
  generated_code = generated_code.rsplit('```', 1)[0].strip()
1510
1534
  else:
1511
- generated_code = llm_response.get("response", "").strip()
1535
+ generated_code = response.strip()
1512
1536
 
1513
1537
  state.messages = llm_response.get("messages", state.messages)
1514
1538
 
@@ -1776,6 +1800,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
1776
1800
  state.current_path = os.getcwd()
1777
1801
 
1778
1802
  display_model = state.chat_model
1803
+ # Ensure state.npc is not None before accessing .model or .provider
1779
1804
  if isinstance(state.npc, NPC) and state.npc.model:
1780
1805
  display_model = state.npc.model
1781
1806
 
@@ -1860,12 +1885,34 @@ def enter_guac_mode(npc=None,
1860
1885
  default_mode_choice=default_mode_choice
1861
1886
  )
1862
1887
 
1863
- project_name = setup_result.get("project_name", "project")
1888
+ project_name = setup_result.get("package_name", "project")
1864
1889
  package_root = setup_result["package_root"]
1865
1890
  package_name = setup_result.get("package_name", "project")
1891
+ npc_team_dir = setup_result.get("npc_team_dir")
1866
1892
 
1893
+ # Always call setup_shell to build history, team, and default_npc
1867
1894
  command_history, default_team, default_npc = setup_shell()
1868
-
1895
+
1896
+ # 🔑 Ensure global guac gets loaded if npc is None
1897
+ if npc is None and default_npc is None:
1898
+ # Construct the path correctly based on where ensure_global_guac_team puts it
1899
+ guac_npc_path = Path(npc_team_dir) / "guac.npc"
1900
+ if guac_npc_path.exists():
1901
+ npc = NPC(file=str(guac_npc_path), db_conn=command_history.engine)
1902
+ # Ensure the team is also correctly set to the global guac team
1903
+ team_ctx_path = Path(npc_team_dir) / "team.ctx"
1904
+ if team_ctx_path.exists():
1905
+ with open(team_ctx_path, "r") as f:
1906
+ team_ctx = yaml.safe_load(f) or {}
1907
+ team = Team(team_path=str(npc_team_dir), forenpc=npc, jinxs={}) # Simplified team creation
1908
+ team.name = team_ctx.get("team_name", "guac_global_team")
1909
+ else:
1910
+ raise RuntimeError(f"No NPC loaded and {guac_npc_path} not found!")
1911
+ elif default_npc and npc is None:
1912
+ # If setup_shell provided a default_npc (e.g., sibiji), ensure it's used
1913
+ npc = default_npc
1914
+
1915
+
1869
1916
  state = ShellState(
1870
1917
  conversation_id=start_new_conversation(),
1871
1918
  stream_output=True,
@@ -1873,8 +1920,8 @@ def enter_guac_mode(npc=None,
1873
1920
  chat_model=os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b"),
1874
1921
  chat_provider=os.environ.get("NPCSH_CHAT_PROVIDER", "ollama"),
1875
1922
  current_path=os.getcwd(),
1876
- npc=npc or default_npc,
1877
- team=team or default_team
1923
+ npc=npc, # This is now guaranteed to be an NPC object
1924
+ team=team or default_team # Use the correctly loaded team or default
1878
1925
  )
1879
1926
 
1880
1927
  state.command_history = command_history
@@ -1889,8 +1936,6 @@ def enter_guac_mode(npc=None,
1889
1936
  print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
1890
1937
 
1891
1938
  run_guac_repl(state, project_name, package_root, package_name)
1892
-
1893
-
1894
1939
 
1895
1940
  def main():
1896
1941
  parser = argparse.ArgumentParser(description="Enter Guac Mode - Interactive Python with LLM assistance.")
@@ -1913,4 +1958,4 @@ def main():
1913
1958
  )
1914
1959
 
1915
1960
  if __name__ == "__main__":
1916
- main()
1961
+ main()
@@ -291,7 +291,7 @@ def guac_handler(command, **kwargs):
291
291
  team = Team(npc_team_dir, db_conn=db_conn)
292
292
 
293
293
 
294
- enter_guac_mode(workspace_dirs,
294
+ enter_guac_mode(
295
295
  npc=npc,
296
296
  team=team,
297
297
  config_dir=config_dir,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.0.22
3
+ Version: 1.0.24
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.22",
81
+ version="1.0.24",
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