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/guac.py CHANGED
@@ -59,7 +59,7 @@ except importlib.metadata.PackageNotFoundError:
59
59
 
60
60
  GUAC_REFRESH_PERIOD = os.environ.get('GUAC_REFRESH_PERIOD', 100)
61
61
  READLINE_HISTORY_FILE = os.path.expanduser("~/.guac_readline_history")
62
- # File extension mapping for organization
62
+
63
63
  EXTENSION_MAP = {
64
64
  "PNG": "images", "JPG": "images", "JPEG": "images", "GIF": "images", "SVG": "images",
65
65
  "MP4": "videos", "AVI": "videos", "MOV": "videos", "WMV": "videos", "MPG": "videos", "MPEG": "videos",
@@ -75,7 +75,7 @@ _guac_monitor_stop_event = None
75
75
  def _clear_readline_buffer():
76
76
  """Clear the current readline input buffer and redisplay prompt."""
77
77
  try:
78
- # Preferred: use Python readline API if available
78
+
79
79
  if hasattr(readline, "replace_line") and hasattr(readline, "redisplay"):
80
80
  readline.replace_line("", 0)
81
81
  readline.redisplay()
@@ -83,11 +83,11 @@ def _clear_readline_buffer():
83
83
  except Exception:
84
84
  pass
85
85
 
86
- # Fallback: call rl_replace_line and rl_redisplay from the linked readline/libedit
86
+
87
87
  try:
88
88
  libname = ctypes.util.find_library("readline") or ctypes.util.find_library("edit") or "readline"
89
89
  rl = ctypes.CDLL(libname)
90
- # rl_replace_line(char *text, int clear_undo)
90
+
91
91
  rl.rl_replace_line.argtypes = [ctypes.c_char_p, ctypes.c_int]
92
92
  rl.rl_redisplay.argtypes = []
93
93
  rl.rl_replace_line(b"", 0)
@@ -113,44 +113,44 @@ def _file_drop_monitor(npc_team_dir: Path, state: ShellState, locals_dict: Dict[
113
113
  time.sleep(poll_interval)
114
114
  continue
115
115
 
116
- # Normalize buffer
116
+
117
117
  candidate = buf.strip()
118
- # If quoted, remove quotes
118
+
119
119
  if (candidate.startswith("'") and candidate.endswith("'")) or (candidate.startswith('"') and candidate.endswith('"')):
120
120
  inner = candidate[1:-1]
121
121
  else:
122
122
  inner = candidate
123
123
 
124
- # quick check: must be single token and existing file
124
+
125
125
  if " " not in inner and Path(inner.replace('~', str(Path.home()))).expanduser().exists() and Path(inner.replace('~', str(Path.home()))).expanduser().is_file():
126
- # Avoid double-processing same buffer
126
+
127
127
  if buf in processed_bufs:
128
128
  time.sleep(poll_interval)
129
129
  continue
130
130
  processed_bufs.add(buf)
131
131
 
132
- # Immediately process: copy and load
132
+
133
133
  try:
134
- # Use your existing handler for multi-file copies to ensure directory structure
135
- # But we want immediate execution for a single file: call _handle_file_drop first to copy
134
+
135
+
136
136
  modified_input, processed_files = _handle_file_drop(buf, npc_team_dir)
137
137
  if processed_files:
138
138
  target_path = processed_files[0]
139
- # Generate loading code based on original file (inner) and target_path
139
+
140
140
  loading_code = _generate_file_analysis_code(inner, target_path)
141
- # Execute via your normal execute_python_code so it records in history
141
+
142
142
  print("\n[guac] Detected file drop — processing automatically...")
143
- # Note: execute_python_code expects state and locals_dict
143
+
144
144
  _state, exec_output = execute_python_code(loading_code, state, locals_dict)
145
- # Print whatever result execute_python_code returned (it will already have been captured)
145
+
146
146
  if exec_output:
147
147
  print(exec_output)
148
- # Clear the current readline buffer so user doesn't have to press Enter
148
+
149
149
  _clear_readline_buffer()
150
150
  except Exception as e:
151
151
  print(f"[guac][ERROR] file drop processing failed: {e}")
152
152
  except Exception:
153
- # Be resilient: don't let thread die
153
+
154
154
  pass
155
155
  time.sleep(poll_interval)
156
156
 
@@ -213,27 +213,27 @@ def execute_python_code(code_str: str, state: ShellState, locals_dict: Dict[str,
213
213
  final_output_str = output_capture.getvalue().strip()
214
214
  output_capture.close()
215
215
 
216
- # ADD THIS LINE:
216
+
217
217
  _capture_plot_state(state.conversation_id, state.command_history.db_path, Path.cwd() / "npc_team")
218
218
 
219
219
  if state.command_history:
220
220
  state.command_history.add_command(code_str, [final_output_str if final_output_str else ""], "", state.current_path)
221
221
  return state, final_output_str
222
222
 
223
- # Modify _generate_file_analysis_code - add the capture call to each code block:
223
+
224
224
  def _generate_file_analysis_code(file_path: str, target_path: str) -> str:
225
225
  """Generate Python code to load and analyze the dropped file"""
226
226
  ext = Path(file_path).suffix.lower()
227
227
  file_var_name = f"file_{datetime.now().strftime('%H%M%S')}"
228
228
 
229
229
  capture_code = f"""
230
- # Capture file analysis state
230
+
231
231
  _capture_file_state('{state.conversation_id}', '{state.command_history.db_path}', r'{target_path}', '''AUTO_GENERATED_CODE''', locals())
232
232
  """
233
233
 
234
234
  if ext == '.pdf':
235
235
  return f"""
236
- # Automatically loaded PDF file
236
+
237
237
  import PyPDF2
238
238
  import pandas as pd
239
239
  try:
@@ -255,7 +255,7 @@ except Exception as e:
255
255
 
256
256
  elif ext in ['.csv']:
257
257
  return f"""
258
- # Automatically loaded CSV file
258
+
259
259
  import pandas as pd
260
260
  try:
261
261
  {file_var_name}_df = pd.read_csv(r'{target_path}')
@@ -272,7 +272,7 @@ except Exception as e:
272
272
 
273
273
  elif ext in ['.xlsx', '.xls']:
274
274
  return f"""
275
- # Automatically loaded Excel file
275
+
276
276
  import pandas as pd
277
277
  try:
278
278
  {file_var_name}_df = pd.read_excel(r'{target_path}')
@@ -289,7 +289,7 @@ except Exception as e:
289
289
 
290
290
  elif ext in ['.json']:
291
291
  return f"""
292
- # Automatically loaded JSON file
292
+
293
293
  import json
294
294
  try:
295
295
  with open(r'{target_path}', 'r') as file:
@@ -308,7 +308,7 @@ except Exception as e:
308
308
 
309
309
  elif ext in ['.txt', '.md']:
310
310
  return f"""
311
- # Automatically loaded text file
311
+
312
312
  try:
313
313
  with open(r'{target_path}', 'r', encoding='utf-8') as file:
314
314
  {file_var_name}_text = file.read()
@@ -324,7 +324,7 @@ except Exception as e:
324
324
 
325
325
  elif ext in ['.png', '.jpg', '.jpeg', '.gif']:
326
326
  return f"""
327
- # Automatically loaded image file
327
+
328
328
  import matplotlib.pyplot as plt
329
329
  from PIL import Image
330
330
  import numpy as np
@@ -349,7 +349,7 @@ except Exception as e:
349
349
 
350
350
  else:
351
351
  return f"""
352
- # Automatically loaded file (unknown type)
352
+
353
353
  try:
354
354
  with open(r'{target_path}', 'rb') as file:
355
355
  {file_var_name}_data = file.read()
@@ -396,7 +396,7 @@ 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
399
+
400
400
  npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
401
401
  npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
402
402
 
@@ -480,7 +480,7 @@ def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None,
480
480
  lang='python', default_mode_choice=None):
481
481
  base_dir = Path.cwd()
482
482
 
483
- # Check if we should default to global without prompting
483
+
484
484
  if GUAC_GLOBAL_FLAG_FILE.exists():
485
485
  print("💡 Using global Guac team as default (previously set).")
486
486
  team_dir = ensure_global_guac_team()
@@ -490,7 +490,7 @@ def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None,
490
490
  "project_description": "Global guac team for analysis.", "package_name": "guac"
491
491
  }
492
492
 
493
- # default: project npc_team_dir
493
+
494
494
  if npc_team_dir is None:
495
495
  npc_team_dir = base_dir / "npc_team"
496
496
  else:
@@ -519,7 +519,7 @@ def setup_guac_mode(config_dir=None, plots_dir=None, npc_team_dir=None,
519
519
  package_name = response if response else "project"
520
520
  except (KeyboardInterrupt, EOFError):
521
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
522
+ GUAC_GLOBAL_FLAG_FILE.touch()
523
523
  team_dir = ensure_global_guac_team()
524
524
  return {
525
525
  "language": lang, "package_root": team_dir, "plots_dir": plots_dir,
@@ -574,7 +574,7 @@ setup(name="{package_name}", version="0.0.1", description="{desc}", packages=fin
574
574
  "project_description": project_description, "package_name": package_name
575
575
  }
576
576
  def setup_npc_team(npc_team_dir, lang, is_subteam=False):
577
- # Create Guac-specific NPCs
577
+
578
578
  guac_npc = {
579
579
  "name": "guac",
580
580
  "primary_directive": (
@@ -602,14 +602,14 @@ def setup_npc_team(npc_team_dir, lang, is_subteam=False):
602
602
 
603
603
  for npc_data in [guac_npc, caug_npc, parsely_npc, toon_npc]:
604
604
  npc_file = npc_team_dir / f"{npc_data['name']}.npc"
605
- if not npc_file.exists(): # Don't overwrite existing NPCs
605
+ if not npc_file.exists():
606
606
  with open(npc_file, "w") as f:
607
607
  yaml.dump(npc_data, f, default_flow_style=False)
608
608
  print(f"Created NPC: {npc_data['name']}")
609
609
  else:
610
610
  print(f"NPC already exists: {npc_data['name']}")
611
611
 
612
- # Only create team.ctx for subteams, otherwise use the main one
612
+
613
613
  if is_subteam:
614
614
  team_ctx_model = os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b")
615
615
  team_ctx_provider = os.environ.get("NPCSH_CHAT_PROVIDER", "ollama")
@@ -644,22 +644,22 @@ def _detect_file_drop(input_text: str) -> bool:
644
644
 
645
645
  stripped = input_text.strip()
646
646
 
647
- # Remove quotes if present
647
+
648
648
  if stripped.startswith("'") and stripped.endswith("'"):
649
649
  stripped = stripped[1:-1]
650
650
  elif stripped.startswith('"') and stripped.endswith('"'):
651
651
  stripped = stripped[1:-1]
652
652
 
653
- # Must be a single token (no spaces) - this is key!
653
+
654
654
  if len(stripped.split()) != 1:
655
655
  return False
656
656
 
657
- # Must not contain Python operators or syntax
657
+
658
658
  python_indicators = ['(', ')', '[', ']', '{', '}', '=', '+', '-', '*', '/', '%', '&', '|', '^', '<', '>', '!', '?', ':', ';', ',']
659
659
  if any(indicator in stripped for indicator in python_indicators):
660
660
  return False
661
661
 
662
- # Must not start with common Python keywords or look like Python
662
+
663
663
  python_keywords = ['import', 'from', 'def', 'class', 'if', 'for', 'while', 'try', 'with', 'lambda', 'print', 'len', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple']
664
664
  if any(stripped.startswith(keyword) for keyword in python_keywords):
665
665
  return False
@@ -670,7 +670,7 @@ from sqlalchemy import create_engine, Column, Integer, String, Text, Float, Date
670
670
  from sqlalchemy.ext.declarative import declarative_base
671
671
  from sqlalchemy.orm import sessionmaker
672
672
 
673
- # Add these classes after your imports
673
+
674
674
  Base = declarative_base()
675
675
 
676
676
  class PlotState(Base):
@@ -704,12 +704,12 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
704
704
  Session = sessionmaker(bind=engine)
705
705
  session = Session()
706
706
 
707
- # Get plot info
707
+
708
708
  fig = plt.gcf()
709
709
  axes = fig.get_axes()
710
710
  data_points = sum(len(line.get_xdata()) for ax in axes for line in ax.get_lines())
711
711
 
712
- # Create hash and check if different from last
712
+
713
713
  plot_hash = hashlib.md5(f"{len(axes)}{data_points}".encode()).hexdigest()
714
714
 
715
715
  last = session.query(PlotState).filter(PlotState.session_id == session_id).order_by(PlotState.timestamp.desc()).first()
@@ -717,13 +717,13 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
717
717
  session.close()
718
718
  return
719
719
 
720
- # Save plot
720
+
721
721
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
722
722
  workspace_dirs = _get_workspace_dirs(npc_team_dir)
723
723
  plot_path = workspace_dirs["plots"] / f"state_{timestamp}.png"
724
724
  plt.savefig(plot_path, dpi=150, bbox_inches='tight')
725
725
 
726
- # Save to DB
726
+
727
727
  plot_state = PlotState(
728
728
  session_id=session_id,
729
729
  plot_hash=plot_hash,
@@ -745,14 +745,14 @@ def _capture_file_state(session_id: str, db_path: str, file_path: str, analysis_
745
745
  Session = sessionmaker(bind=engine)
746
746
  session = Session()
747
747
 
748
- # Get file hash
748
+
749
749
  try:
750
750
  with open(file_path, 'rb') as f:
751
751
  file_hash = hashlib.md5(f.read()).hexdigest()
752
752
  except:
753
753
  file_hash = "unknown"
754
754
 
755
- # Get variables created
755
+
756
756
  file_stem = Path(file_path).stem.lower()
757
757
  vars_created = [k for k in locals_dict.keys() if not k.startswith('_') and file_stem in k.lower()]
758
758
 
@@ -812,7 +812,7 @@ def _generate_file_analysis_code(file_path: str, target_path: str) -> str:
812
812
 
813
813
  if ext == '.pdf':
814
814
  return f"""
815
- # Automatically loaded PDF file
815
+
816
816
  import PyPDF2
817
817
  import pandas as pd
818
818
  try:
@@ -833,7 +833,7 @@ except Exception as e:
833
833
 
834
834
  elif ext in ['.csv']:
835
835
  return f"""
836
- # Automatically loaded CSV file
836
+
837
837
  import pandas as pd
838
838
  try:
839
839
  {file_var_name}_df = pd.read_csv(r'{target_path}')
@@ -849,7 +849,7 @@ except Exception as e:
849
849
 
850
850
  elif ext in ['.xlsx', '.xls']:
851
851
  return f"""
852
- # Automatically loaded Excel file
852
+
853
853
  import pandas as pd
854
854
  try:
855
855
  {file_var_name}_df = pd.read_excel(r'{target_path}')
@@ -865,7 +865,7 @@ except Exception as e:
865
865
 
866
866
  elif ext in ['.json']:
867
867
  return f"""
868
- # Automatically loaded JSON file
868
+
869
869
  import json
870
870
  try:
871
871
  with open(r'{target_path}', 'r') as file:
@@ -883,7 +883,7 @@ except Exception as e:
883
883
 
884
884
  elif ext in ['.txt', '.md']:
885
885
  return f"""
886
- # Automatically loaded text file
886
+
887
887
  try:
888
888
  with open(r'{target_path}', 'r', encoding='utf-8') as file:
889
889
  {file_var_name}_text = file.read()
@@ -898,7 +898,7 @@ except Exception as e:
898
898
 
899
899
  elif ext in ['.png', '.jpg', '.jpeg', '.gif']:
900
900
  return f"""
901
- # Automatically loaded image file
901
+
902
902
  import matplotlib.pyplot as plt
903
903
  from PIL import Image
904
904
  import numpy as np
@@ -922,7 +922,7 @@ except Exception as e:
922
922
 
923
923
  else:
924
924
  return f"""
925
- # Automatically loaded file (unknown type)
925
+
926
926
  try:
927
927
  with open(r'{target_path}', 'rb') as file:
928
928
  {file_var_name}_data = file.read()
@@ -935,9 +935,9 @@ except Exception as e:
935
935
  """
936
936
  def _handle_file_drop(input_text: str, npc_team_dir: Path) -> Tuple[str, List[str]]:
937
937
  """Handle file drops by copying files to appropriate workspace directories"""
938
- #print(f"[DEBUG] _handle_file_drop called with input: '{input_text}'")
938
+
939
939
 
940
- # Immediately check if this is a single file path
940
+
941
941
  stripped = input_text.strip("'\"")
942
942
  if os.path.exists(stripped) and os.path.isfile(stripped):
943
943
  print(f"[DEBUG] Direct file drop detected: {stripped}")
@@ -959,11 +959,11 @@ def _handle_file_drop(input_text: str, npc_team_dir: Path) -> Tuple[str, List[st
959
959
  shutil.copy2(expanded_path, target_path)
960
960
  print(f"📁 Copied {expanded_path.name} to workspace: {target_path}")
961
961
 
962
- # Generate and execute loading code
962
+
963
963
  loading_code = _generate_file_analysis_code(str(expanded_path), str(target_path))
964
964
  print(f"\n# Auto-generated file loading code:\n---\n{loading_code}\n---\n")
965
965
 
966
- # Actually execute the loading code
966
+
967
967
  exec(loading_code)
968
968
 
969
969
  return "", [str(target_path)]
@@ -971,12 +971,12 @@ def _handle_file_drop(input_text: str, npc_team_dir: Path) -> Tuple[str, List[st
971
971
  print(f"[ERROR] Failed to process file drop: {e}")
972
972
  return input_text, []
973
973
 
974
- # Existing multi-file handling logic
974
+
975
975
  processed_files = []
976
976
  file_paths = re.findall(r"'([^']+)'|\"([^\"]+)\"|(\S+)", input_text)
977
977
  file_paths = [path for group in file_paths for path in group if path]
978
978
 
979
- #print(f"[DEBUG] Found file paths: {file_paths}")
979
+
980
980
 
981
981
  if not file_paths:
982
982
 
@@ -998,12 +998,12 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
998
998
  Session = sessionmaker(bind=engine)
999
999
  session = Session()
1000
1000
 
1001
- # Get plot info
1001
+
1002
1002
  fig = plt.gcf()
1003
1003
  axes = fig.get_axes()
1004
1004
  data_points = sum(len(line.get_xdata()) for ax in axes for line in ax.get_lines())
1005
1005
 
1006
- # Create hash and check if different from last
1006
+
1007
1007
  plot_hash = hashlib.md5(f"{len(axes)}{data_points}".encode()).hexdigest()
1008
1008
 
1009
1009
  last = session.query(PlotState).filter(PlotState.session_id == session_id).order_by(PlotState.timestamp.desc()).first()
@@ -1011,13 +1011,13 @@ def _capture_plot_state(session_id: str, db_path: str, npc_team_dir: Path):
1011
1011
  session.close()
1012
1012
  return
1013
1013
 
1014
- # Save plot
1014
+
1015
1015
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1016
1016
  workspace_dirs = _get_workspace_dirs(npc_team_dir)
1017
1017
  plot_path = workspace_dirs["plots"] / f"state_{timestamp}.png"
1018
1018
  plt.savefig(plot_path, dpi=150, bbox_inches='tight')
1019
1019
 
1020
- # Save to DB
1020
+
1021
1021
  plot_state = PlotState(
1022
1022
  session_id=session_id,
1023
1023
  plot_hash=plot_hash,
@@ -1043,14 +1043,14 @@ def _capture_file_state(session_id: str, db_path: str, file_path: str, analysis_
1043
1043
  Session = sessionmaker(bind=engine)
1044
1044
  session = Session()
1045
1045
 
1046
- # Get file hash
1046
+
1047
1047
  try:
1048
1048
  with open(file_path, 'rb') as f:
1049
1049
  file_hash = hashlib.md5(f.read()).hexdigest()
1050
1050
  except:
1051
1051
  file_hash = "unknown"
1052
1052
 
1053
- # Get variables created
1053
+
1054
1054
  file_stem = Path(file_path).stem.lower()
1055
1055
  vars_created = [k for k in locals_dict.keys() if not k.startswith('_') and file_stem in k.lower()]
1056
1056
 
@@ -1143,28 +1143,28 @@ import sys
1143
1143
  from io import StringIO
1144
1144
  from contextlib import redirect_stdout, redirect_stderr
1145
1145
 
1146
- # --- New Emoji Function for Agentic Mode ---
1146
+
1147
1147
  def _get_guac_agent_emoji(failures: int, max_fail: int = 3) -> str:
1148
1148
  """
1149
1149
  Returns an avocado emoji representing the state based on consecutive failures.
1150
1150
  Includes "puke" emoji for max_fail, and "skull" for exceeding max_fail + 20.
1151
1151
  """
1152
1152
  if failures == 0:
1153
- return "🥑" # Fresh
1153
+ return "🥑"
1154
1154
  elif failures == 1:
1155
- return "🥑🔪" # Sliced, contemplating next steps
1155
+ return "🥑🔪"
1156
1156
  elif failures == 2:
1157
- return "🥑🥣" # In the bowl, getting mashed
1157
+ return "🥑🥣"
1158
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
1159
+ return "🥑🤢"
1160
+ elif failures > max_fail + 20:
1161
+ return "🥑💀"
1162
1162
  elif failures > max_fail:
1163
- return "🥑🟤" # Bruised and bad
1163
+ return "🥑🟤"
1164
1164
  else:
1165
- return "🥑❓" # Unknown state
1165
+ return "🥑❓"
1166
+
1166
1167
 
1167
- # --- New Helper for Persisting Global Choice ---
1168
1168
  GUAC_GLOBAL_FLAG_FILE = Path.home() / ".npcsh" / ".guac_use_global"
1169
1169
 
1170
1170
 
@@ -1173,14 +1173,14 @@ def _run_agentic_mode(command: str,
1173
1173
  locals_dict: Dict[str, Any],
1174
1174
  npc_team_dir: Path) -> Tuple[ShellState, Any]:
1175
1175
  """Run agentic mode with continuous iteration based on progress"""
1176
- max_iterations = 5 # Increased slightly for more complex tasks
1176
+ max_iterations = 5
1177
1177
  iteration = 0
1178
1178
  full_output = []
1179
1179
  current_command = command
1180
1180
  consecutive_failures = 0
1181
- max_consecutive_failures = 3 # This is the limit before stopping
1181
+ max_consecutive_failures = 3
1182
1182
 
1183
- # Build context of existing variables
1183
+
1184
1184
  existing_vars_context = "EXISTING VARIABLES IN ENVIRONMENT:\n"
1185
1185
  for var_name, var_value in locals_dict.items():
1186
1186
  if not var_name.startswith('_') and var_name not in ['In', 'Out', 'exit', 'quit', 'get_ipython']:
@@ -1259,7 +1259,8 @@ def _run_agentic_mode(command: str,
1259
1259
  llm_response = get_llm_response(prompt,
1260
1260
  npc=state.npc,
1261
1261
  stream=True,
1262
- messages=state.messages)
1262
+ messages=state.messages,
1263
+ thinking=False)
1263
1264
 
1264
1265
  generated_code = print_and_process_stream(llm_response.get('response'),
1265
1266
  npc_model,
@@ -1270,12 +1271,14 @@ def _run_agentic_mode(command: str,
1270
1271
  state.messages.append({'role':'assistant', 'content': generated_code})
1271
1272
 
1272
1273
  if '<request_for_input>' in generated_code:
1274
+
1273
1275
  generated_code = generated_code.split('>')[1].split('<')[0]
1274
1276
  user_feedback = input("\n🤔 Agent requests feedback (press Enter to continue or type your input): ").strip()
1275
1277
  current_command = f"{current_command} - User feedback: {user_feedback}"
1276
1278
  max_iterations += int(max_iterations/2)
1277
1279
  continue
1278
-
1280
+ if '<think>' in generated_code and '</think>' in generated_code:
1281
+ generated_code = generated_code.split('</think>')[1]
1279
1282
  if generated_code.startswith('```python'):
1280
1283
  generated_code = generated_code[len('```python'):].strip()
1281
1284
  if generated_code.endswith('```'):
@@ -1417,16 +1420,16 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
1417
1420
 
1418
1421
 
1419
1422
 
1420
- # Check if this is a file drop (single file path)
1423
+
1421
1424
  if _detect_file_drop(stripped_command):
1422
1425
  if stripped_command.startswith('run'):
1423
1426
  pass
1424
1427
  else:
1425
- # Clean the path
1428
+
1426
1429
  file_path = stripped_command.strip("'\"")
1427
1430
  expanded_path = Path(file_path).resolve()
1428
1431
 
1429
- # Copy to workspace
1432
+
1430
1433
  workspace_dirs = _get_workspace_dirs(npc_team_dir)
1431
1434
  _ensure_workspace_dirs(workspace_dirs)
1432
1435
 
@@ -1442,7 +1445,7 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
1442
1445
  shutil.copy2(expanded_path, target_path)
1443
1446
  print(f"📁 Copied {expanded_path.name} to workspace: {target_path}")
1444
1447
 
1445
- # Generate and execute loading code
1448
+
1446
1449
  loading_code = _generate_file_analysis_code(str(expanded_path), str(target_path))
1447
1450
  print(f"\n# Auto-generated file loading code:\n---\n{loading_code}\n---\n")
1448
1451
 
@@ -1452,25 +1455,25 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
1452
1455
  print(f"[ERROR] Failed to copy or load file: {e}")
1453
1456
  return state, f"Error loading file: {e}"
1454
1457
 
1455
- # Handle file drops in text (multiple files or files with other text)
1458
+
1456
1459
  processed_command, processed_files, file_paths = _handle_file_drop(stripped_command, npc_team_dir)
1457
1460
  if processed_files:
1458
1461
  print(f"📁 Processed {len(processed_files)} files")
1459
1462
  stripped_command = processed_command + 'Here are the files associated with the request'
1460
1463
 
1461
- # Handle /refresh command
1464
+
1462
1465
  if stripped_command == "/refresh":
1463
1466
  _handle_guac_refresh(state, project_name, src_dir)
1464
1467
  return state, "Refresh process initiated."
1465
1468
 
1466
- # Handle mode switching commands
1469
+
1467
1470
  if stripped_command in ["/agent", "/chat", "/cmd"]:
1468
1471
  state.current_mode = stripped_command[1:]
1469
1472
  return state, f"Switched to {state.current_mode.upper()} mode."
1470
1473
 
1471
1474
 
1472
1475
 
1473
- # Check if it's a router command (starts with / and not a built-in command)
1476
+
1474
1477
  if stripped_command.startswith('/') and stripped_command not in ["/refresh", "/agent", "/chat", "/cmd"]:
1475
1478
  return execute_command(stripped_command, state, review=True, router=router)
1476
1479
  if is_python_code(stripped_command):
@@ -1484,7 +1487,7 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
1484
1487
  return _run_agentic_mode(stripped_command, state, locals_dict, npc_team_dir)
1485
1488
  if state.current_mode == "cmd":
1486
1489
 
1487
- # If not Python, use LLM to generate Python code
1490
+
1488
1491
  locals_context_string = "Current Python environment variables and functions:\n"
1489
1492
  if locals_dict:
1490
1493
  for k, v in locals_dict.items():
@@ -1493,14 +1496,14 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
1493
1496
  value_repr = repr(v)
1494
1497
  if len(value_repr) > 200:
1495
1498
  value_repr = value_repr[:197] + "..."
1496
- locals_context_string += f"- {k} (type: {type(v).__name__}) = {value_repr}\n"
1499
+ loaals_context_string += f"- {k} (type: {type(v).__name__}) = {value_repr}\n"
1497
1500
  except Exception:
1498
1501
  locals_context_string += f"- {k} (type: {type(v).__name__}) = <unrepresentable>\n"
1499
1502
  locals_context_string += "\n--- End of Environment Context ---\n"
1500
1503
  else:
1501
1504
  locals_context_string += "(Environment is empty)\n"
1502
1505
 
1503
- # ADD CONTEXT ENHANCEMENT HERE:
1506
+
1504
1507
  enhanced_prompt = stripped_command
1505
1508
  if any(word in stripped_command.lower() for word in ['plot', 'graph', 'chart', 'figure', 'visualiz']):
1506
1509
  plot_context = _get_plot_context(state.conversation_id, state.command_history.db_path)
@@ -1516,7 +1519,7 @@ def execute_guac_command(command: str, state: ShellState, locals_dict: Dict[str,
1516
1519
  {locals_context_string}
1517
1520
  Begin directly with the code
1518
1521
  """
1519
- # Ensure state.npc is not None before accessing .model or .provider
1522
+
1520
1523
  npc_model = state.npc.model if state.npc and state.npc.model else state.chat_model
1521
1524
  npc_provider = state.npc.provider if state.npc and state.npc.provider else state.chat_provider
1522
1525
 
@@ -1558,7 +1561,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
1558
1561
  from npcsh.routes import router
1559
1562
 
1560
1563
 
1561
- # Get workspace info
1564
+
1562
1565
  npc_team_dir = Path.cwd() / "npc_team"
1563
1566
  workspace_dirs = _get_workspace_dirs(npc_team_dir)
1564
1567
  _ensure_workspace_dirs(workspace_dirs)
@@ -1610,10 +1613,10 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
1610
1613
  return None
1611
1614
 
1612
1615
  try:
1613
- # Try using npcpy's load_file_contents first for specialized formats
1616
+
1614
1617
  file_ext = path.suffix.upper().lstrip('.')
1615
1618
  if file_ext in ['PDF', 'DOCX', 'PPTX', 'HTML', 'HTM', 'CSV', 'XLS', 'XLSX', 'JSON']:
1616
- chunks = load_file_contents(str(path), chunk_size=10000) # Large chunk to get full content
1619
+ chunks = load_file_contents(str(path), chunk_size=10000)
1617
1620
  if chunks and not chunks[0].startswith("Error") and not chunks[0].startswith("Unsupported"):
1618
1621
  content = '\n'.join(chunks)
1619
1622
  lines = content.split('\n')
@@ -1632,7 +1635,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
1632
1635
  print(f"End of {path.name}")
1633
1636
  return content
1634
1637
 
1635
- # Fall back to regular text reading
1638
+
1636
1639
  with open(path, 'r', encoding=encoding) as f:
1637
1640
  lines = []
1638
1641
  for i, line in enumerate(f, 1):
@@ -1680,10 +1683,10 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
1680
1683
  """
1681
1684
  path = Path(file_path).expanduser().resolve()
1682
1685
 
1683
- # Create parent directories if needed
1686
+
1684
1687
  path.parent.mkdir(parents=True, exist_ok=True)
1685
1688
 
1686
- # Backup original if it exists
1689
+
1687
1690
  if backup and path.exists():
1688
1691
  backup_path = path.with_suffix(path.suffix + '.backup')
1689
1692
  import shutil
@@ -1691,7 +1694,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
1691
1694
  print(f"Backup saved: {backup_path.name}")
1692
1695
 
1693
1696
  try:
1694
- # Read existing content if file exists
1697
+
1695
1698
  existing_lines = []
1696
1699
  if path.exists():
1697
1700
  with open(path, 'r', encoding='utf-8') as f:
@@ -1801,7 +1804,7 @@ def run_guac_repl(state: ShellState, project_name: str, package_root: Path, pack
1801
1804
  state.current_path = os.getcwd()
1802
1805
 
1803
1806
  display_model = state.chat_model
1804
- # Ensure state.npc is not None before accessing .model or .provider
1807
+
1805
1808
  if isinstance(state.npc, NPC) and state.npc.model:
1806
1809
  display_model = state.npc.model
1807
1810
 
@@ -1891,26 +1894,26 @@ def enter_guac_mode(npc=None,
1891
1894
  package_name = setup_result.get("package_name", "project")
1892
1895
  npc_team_dir = setup_result.get("npc_team_dir")
1893
1896
 
1894
- # Always call setup_shell to build history, team, and default_npc
1897
+
1895
1898
  command_history, default_team, default_npc = setup_shell()
1896
1899
 
1897
- # 🔑 Ensure global guac gets loaded if npc is None
1900
+
1898
1901
  if npc is None and default_npc is None:
1899
- # Construct the path correctly based on where ensure_global_guac_team puts it
1902
+
1900
1903
  guac_npc_path = Path(npc_team_dir) / "guac.npc"
1901
1904
  if guac_npc_path.exists():
1902
1905
  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
1906
+
1904
1907
  team_ctx_path = Path(npc_team_dir) / "team.ctx"
1905
1908
  if team_ctx_path.exists():
1906
1909
  with open(team_ctx_path, "r") as f:
1907
1910
  team_ctx = yaml.safe_load(f) or {}
1908
- team = Team(team_path=str(npc_team_dir), forenpc=npc, jinxs={}) # Simplified team creation
1911
+ team = Team(team_path=str(npc_team_dir), forenpc=npc, jinxs={})
1909
1912
  team.name = team_ctx.get("team_name", "guac_global_team")
1910
1913
  else:
1911
1914
  raise RuntimeError(f"No NPC loaded and {guac_npc_path} not found!")
1912
1915
  elif default_npc and npc is None:
1913
- # If setup_shell provided a default_npc (e.g., sibiji), ensure it's used
1916
+
1914
1917
  npc = default_npc
1915
1918
 
1916
1919
 
@@ -1921,8 +1924,8 @@ def enter_guac_mode(npc=None,
1921
1924
  chat_model=os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b"),
1922
1925
  chat_provider=os.environ.get("NPCSH_CHAT_PROVIDER", "ollama"),
1923
1926
  current_path=os.getcwd(),
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
1927
+ npc=npc,
1928
+ team=team or default_team
1926
1929
  )
1927
1930
 
1928
1931
  state.command_history = command_history