npcsh 1.1.21__py3-none-any.whl → 1.1.22__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 (136) hide show
  1. npcsh/_state.py +10 -5
  2. npcsh/benchmark/npcsh_agent.py +22 -14
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +2 -2
  4. npcsh/mcp_server.py +9 -1
  5. npcsh/npc_team/alicanto.npc +12 -6
  6. npcsh/npc_team/corca.npc +0 -1
  7. npcsh/npc_team/frederic.npc +2 -3
  8. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +83 -61
  9. npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
  10. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  11. npcsh/npc_team/jinxs/modes/convene.jinx +597 -0
  12. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  13. npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
  14. npcsh/npc_team/jinxs/modes/plonk.jinx +16 -7
  15. npcsh/npc_team/jinxs/modes/yap.jinx +628 -187
  16. npcsh/npc_team/kadiefa.npc +2 -1
  17. npcsh/npc_team/sibiji.npc +3 -3
  18. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
  19. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.npc +12 -6
  20. npcsh-1.1.22.data/data/npcsh/npc_team/build.jinx +378 -0
  21. npcsh-1.1.22.data/data/npcsh/npc_team/corca.jinx +820 -0
  22. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.npc +0 -1
  23. npcsh-1.1.22.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  24. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic.npc +2 -3
  25. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.npc +2 -1
  26. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kg.jinx +69 -2
  27. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.jinx +16 -7
  28. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.npc +3 -3
  29. npcsh-1.1.22.data/data/npcsh/npc_team/yap.jinx +716 -0
  30. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/METADATA +246 -281
  31. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/RECORD +127 -130
  32. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
  33. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  34. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  35. npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
  36. npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
  37. npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  38. npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
  39. npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
  40. npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
  41. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  42. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  43. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  44. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.png +0 -0
  45. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  46. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  47. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  48. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  49. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/chat.jinx +0 -0
  50. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/click.jinx +0 -0
  51. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  52. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  53. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  54. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  55. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/compile.jinx +0 -0
  56. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/compress.jinx +0 -0
  57. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/config_tui.jinx +0 -0
  58. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  59. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/convene.jinx +0 -0
  60. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.png +0 -0
  61. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca_example.png +0 -0
  62. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  63. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  64. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  65. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  66. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic4.png +0 -0
  67. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/git.jinx +0 -0
  68. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.jinx +0 -0
  69. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.npc +0 -0
  70. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.png +0 -0
  71. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/help.jinx +0 -0
  72. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  73. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/init.jinx +0 -0
  74. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
  75. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  76. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  77. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  78. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  79. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  80. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/memories.jinx +0 -0
  81. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/models.jinx +0 -0
  82. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  83. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/notify.jinx +0 -0
  84. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  85. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  86. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/nql.jinx +0 -0
  87. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  88. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  89. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/ots.jinx +0 -0
  90. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/papers.jinx +0 -0
  91. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/paste.jinx +0 -0
  92. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.npc +0 -0
  93. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.png +0 -0
  94. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  95. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/pti.jinx +0 -0
  96. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/python.jinx +0 -0
  97. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  98. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  99. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/roll.jinx +0 -0
  100. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  101. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sample.jinx +0 -0
  102. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  103. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  104. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/serve.jinx +0 -0
  105. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/set.jinx +0 -0
  106. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/setup.jinx +0 -0
  107. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sh.jinx +0 -0
  108. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/shh.jinx +0 -0
  109. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.png +0 -0
  110. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  111. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  112. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.jinx +0 -0
  113. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.png +0 -0
  114. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sql.jinx +0 -0
  115. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch.jinx +0 -0
  116. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  117. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  118. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switches.jinx +0 -0
  119. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sync.jinx +0 -0
  120. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/team.jinx +0 -0
  121. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  122. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  123. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  124. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/usage.jinx +0 -0
  125. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  126. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  127. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/wait.jinx +0 -0
  128. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/wander.jinx +0 -0
  129. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  130. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  131. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/yap.png +0 -0
  132. {npcsh-1.1.21.data → npcsh-1.1.22.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  133. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/WHEEL +0 -0
  134. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/entry_points.txt +0 -0
  135. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/licenses/LICENSE +0 -0
  136. {npcsh-1.1.21.dist-info → npcsh-1.1.22.dist-info}/top_level.txt +0 -0
npcsh/_state.py CHANGED
@@ -2537,19 +2537,16 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
2537
2537
  elif npc_obj and getattr(npc_obj, "tool_map", None):
2538
2538
  tool_map.update(npc_obj.tool_map)
2539
2539
 
2540
- # Jinx tools from NPC and Team
2540
+ # Jinx tools from NPC only (NPC.jinxs_dict is already filtered by jinxs_spec
2541
+ # during initialize_jinxs - don't add the full team catalog which overwhelms small models)
2541
2542
  aggregated_jinxs: Dict[str, Any] = {}
2542
2543
  if npc_obj and getattr(npc_obj, "jinxs_dict", None):
2543
2544
  aggregated_jinxs.update(npc_obj.jinxs_dict)
2544
- if state.team and isinstance(state.team, Team) and getattr(state.team, "jinxs_dict", None):
2545
- aggregated_jinxs.update({k: v for k, v in state.team.jinxs_dict.items() if k not in aggregated_jinxs})
2546
2545
 
2547
2546
  if aggregated_jinxs:
2548
2547
  jinx_catalog: Dict[str, Dict[str, Any]] = {}
2549
2548
  if npc_obj and getattr(npc_obj, "jinx_tool_catalog", None):
2550
2549
  jinx_catalog.update(npc_obj.jinx_tool_catalog or {})
2551
- if state.team and isinstance(state.team, Team) and getattr(state.team, "jinx_tool_catalog", None):
2552
- jinx_catalog.update(state.team.jinx_tool_catalog or {})
2553
2550
  if not jinx_catalog:
2554
2551
  jinx_catalog = build_jinx_tool_catalog(aggregated_jinxs)
2555
2552
 
@@ -2898,6 +2895,14 @@ def process_pipeline_command(
2898
2895
  tools_for_llm, tool_exec_map = collect_llm_tools(state)
2899
2896
  if not tools_for_llm:
2900
2897
  tool_capable = False
2898
+ else:
2899
+ # Add tool guidance so model knows to use function calls
2900
+ tool_names = [t['function']['name'] for t in tools_for_llm if 'function' in t]
2901
+ info += (
2902
+ f"\nYou have access to these tools: {', '.join(tool_names)}. "
2903
+ f"You MUST use the function calling interface to invoke them. "
2904
+ f"Do NOT write tool names as text - call them as functions."
2905
+ )
2901
2906
 
2902
2907
  npc_name = (
2903
2908
  state.npc.name
@@ -53,11 +53,11 @@ class NpcshAgent(BaseInstalledAgent):
53
53
  Returns:
54
54
  List of ExecInput commands to execute
55
55
  """
56
- # Wrap the instruction with explicit jinx usage directions and retry logic
57
- tool_instruction = f"""You have access to jinxs including edit_file (for writing/creating files), sh (for running shell commands), and python (for running Python code).
56
+ # Wrap the instruction with tool usage directions and retry logic
57
+ tool_instruction = f"""You have access to tools: edit_file (for writing/creating files), sh (for running shell commands), and python (for running Python code).
58
58
 
59
59
  IMPORTANT RULES:
60
- 1. You MUST use these jinxs to complete the task. Do NOT just output code as text - use the edit_file jinx to actually write files to disk.
60
+ 1. You MUST call these tools using the function calling interface to complete the task. Do NOT write tool names as text - invoke them as function calls.
61
61
  2. After implementing a solution, you MUST verify it works by running any provided test scripts.
62
62
  3. If a test fails or produces an error, you MUST try a DIFFERENT approach. Do not give up.
63
63
  4. Keep trying different approaches until you succeed or have tried at least 10 different solutions.
@@ -66,13 +66,11 @@ IMPORTANT RULES:
66
66
  Task: {instruction}
67
67
 
68
68
  WORKFLOW:
69
- 1. Implement your solution using edit_file and sh
69
+ 1. Call edit_file to write code files. Call sh to run commands.
70
70
  2. Run any test scripts mentioned in the task
71
71
  3. Check the output carefully - look for "PASS", "SUCCESS", "OK" or similar
72
72
  4. If the test failed, analyze why and try a completely different approach
73
- 5. Repeat until the test passes
74
-
75
- Remember: Use edit_file to write code files. Use sh to run commands. VERIFY your solution works before concluding."""
73
+ 5. Repeat until the test passes"""
76
74
 
77
75
  escaped_instruction = shlex.quote(tool_instruction)
78
76
  model_name = self.model_name
@@ -146,8 +144,14 @@ Remember: Use edit_file to write code files. Use sh to run commands. VERIFY your
146
144
  # Using corca NPC which has edit_file tool for writing files
147
145
  # Using the npc CLI which supports single-command execution
148
146
  # NPCSH_DEFAULT_MODE=agent enables automatic tool execution
147
+ ollama_env = ""
148
+ if npcsh_provider == "ollama":
149
+ ollama_host = os.environ.get("OLLAMA_HOST", "http://host.docker.internal:11434")
150
+ ollama_env = f'OLLAMA_HOST="{ollama_host}" '
151
+
149
152
  npcsh_cmd = (
150
153
  f'{env_prefix}'
154
+ f'{ollama_env}'
151
155
  f'NPCSH_CHAT_MODEL="{model}" '
152
156
  f'NPCSH_CHAT_PROVIDER="{npcsh_provider}" '
153
157
  f'NPCSH_STREAM_OUTPUT=0 '
@@ -234,11 +238,11 @@ class NpcshAgentWithNpc(NpcshAgent):
234
238
 
235
239
  def create_run_agent_commands(self, instruction: str) -> list:
236
240
  """Create commands using a specific NPC."""
237
- # Wrap the instruction with explicit jinx usage directions and retry logic
238
- tool_instruction = f"""You have access to jinxs including edit_file (for writing/creating files), sh (for running shell commands), and python (for running Python code).
241
+ # Wrap the instruction with tool usage directions and retry logic
242
+ tool_instruction = f"""You have access to tools: edit_file (for writing/creating files), sh (for running shell commands), and python (for running Python code).
239
243
 
240
244
  IMPORTANT RULES:
241
- 1. You MUST use these jinxs to complete the task. Do NOT just output code as text - use the edit_file jinx to actually write files to disk.
245
+ 1. You MUST call these tools using the function calling interface to complete the task. Do NOT write tool names as text - invoke them as function calls.
242
246
  2. After implementing a solution, you MUST verify it works by running any provided test scripts.
243
247
  3. If a test fails or produces an error, you MUST try a DIFFERENT approach. Do not give up.
244
248
  4. Keep trying different approaches until you succeed or have tried at least 10 different solutions.
@@ -247,13 +251,11 @@ IMPORTANT RULES:
247
251
  Task: {instruction}
248
252
 
249
253
  WORKFLOW:
250
- 1. Implement your solution using edit_file and sh
254
+ 1. Call edit_file to write code files. Call sh to run commands.
251
255
  2. Run any test scripts mentioned in the task
252
256
  3. Check the output carefully - look for "PASS", "SUCCESS", "OK" or similar
253
257
  4. If the test failed, analyze why and try a completely different approach
254
- 5. Repeat until the test passes
255
-
256
- Remember: Use edit_file to write code files. Use sh to run commands. VERIFY your solution works before concluding."""
258
+ 5. Repeat until the test passes"""
257
259
 
258
260
  escaped_instruction = shlex.quote(tool_instruction)
259
261
  model_name = self.model_name
@@ -309,8 +311,14 @@ Remember: Use edit_file to write code files. Use sh to run commands. VERIFY your
309
311
 
310
312
  # Use specific NPC with --npc flag
311
313
  # NPCSH_DEFAULT_MODE=agent enables automatic tool execution
314
+ ollama_env = ""
315
+ if npcsh_provider == "ollama":
316
+ ollama_host = os.environ.get("OLLAMA_HOST", "http://host.docker.internal:11434")
317
+ ollama_env = f'OLLAMA_HOST="{ollama_host}" '
318
+
312
319
  npcsh_cmd = (
313
320
  f'{env_prefix}'
321
+ f'{ollama_env}'
314
322
  f'NPCSH_CHAT_MODEL="{model}" '
315
323
  f'NPCSH_CHAT_PROVIDER="{npcsh_provider}" '
316
324
  f'NPCSH_STREAM_OUTPUT=0 '
@@ -14,8 +14,8 @@ fi
14
14
 
15
15
  # Install npcsh with lite dependencies (API providers only, no local models)
16
16
  # Use --break-system-packages for PEP 668 compliance (Ubuntu 24.04+)
17
- echo "Installing npcsh[lite]..."
18
- pip install --quiet --break-system-packages npcsh[lite] || pip install --quiet npcsh[lite]
17
+ echo "Installing npcsh[lite] + ollama..."
18
+ pip install --quiet --break-system-packages npcsh[lite] ollama || pip install --quiet npcsh[lite] ollama
19
19
 
20
20
  # Verify installation
21
21
  echo "Verifying npcsh installation..."
npcsh/mcp_server.py CHANGED
@@ -1,9 +1,17 @@
1
1
 
2
2
  """
3
- Enhanced MCP server that incorporates functionality from npcpy.routes,
3
+ Enhanced MCP server that incorporates functionality from npcpy.routes,
4
4
  npcpy.llm_funcs, and npcpy.npc_compiler as tools.
5
5
  """
6
6
 
7
+ # When run as a subprocess, Python adds the script directory to sys.path[0].
8
+ # Since this file lives inside the npcsh package, that shadows the package
9
+ # (npcsh.py is found instead of the npcsh/ package). Remove it.
10
+ import sys as _sys, os as _os
11
+ _script_dir = _os.path.dirname(_os.path.abspath(__file__))
12
+ if _script_dir in _sys.path:
13
+ _sys.path.remove(_script_dir)
14
+
7
15
  import os
8
16
  import subprocess
9
17
  import json
@@ -11,13 +11,19 @@ colors:
11
11
  top: "255,215,0"
12
12
  bottom: "218,165,32"
13
13
  primary_directive: |
14
- You are alicanto, the research and exploration specialist of the NPC team.
15
- Like the mythical bird, you lead users to discover valuable information.
16
- Your role is web research, searching, and helping users explore topics.
17
- Use search tools to find information and present findings clearly.
14
+ You are alicanto, a research agent. You investigate hypotheses through experimentation and evidence gathering.
15
+ Search academic papers to ground your work in existing literature.
16
+ Search the web for data, documentation, and recent findings.
17
+ Write and execute Python code to analyze data, compute statistics, and generate results.
18
+ Use shell commands for data processing and system tasks.
19
+ Create files to record your findings, analyses, and evidence.
20
+ When exploring a hypothesis, gather evidence from multiple sources, analyze it quantitatively where possible, and document what you find.
21
+ Say RESEARCH_COMPLETE when you have sufficient evidence to evaluate your hypothesis.
18
22
  jinxs:
19
- - lib/core/search
23
+ - lib/core/search/web_search
24
+ - lib/core/search/file_search
25
+ - lib/core/search/db_search
20
26
  - lib/core/sh
21
27
  - lib/core/python
22
28
  - lib/core/load_file
23
- - lib/research/*
29
+ - lib/core/edit_file
npcsh/npc_team/corca.npc CHANGED
@@ -27,5 +27,4 @@ jinxs:
27
27
  - lib/core/python
28
28
  - lib/core/edit_file
29
29
  - lib/core/load_file
30
- - lib/core/search
31
30
 
@@ -22,6 +22,5 @@ jinxs:
22
22
  - lib/core/sql
23
23
  - lib/core/sh
24
24
  - lib/core/load_file
25
- - lib/core/search
26
- - lib/gen/*
27
- - bin/wander
25
+ - lib/core/search/web_search
26
+ - lib/core/edit_file
@@ -1,6 +1,6 @@
1
1
  jinx_name: edit_file
2
- description: Examines a file, determines what changes are needed, and applies those
3
- changes.
2
+ description: Creates or edits a file. If the file does not exist, creates it with
3
+ the specified content. If the file exists, examines it and applies changes.
4
4
  inputs:
5
5
  - file_path
6
6
  - edit_instructions
@@ -17,19 +17,42 @@ steps:
17
17
  edit_instructions = {{ edit_instructions | string | tojson }}
18
18
  backup_str = {{ backup | default("true") | string | tojson }}
19
19
  create_backup = backup_str.lower() not in ('false', 'no', '0', '')
20
-
21
-
22
- with open(file_path, 'r') as f:
23
- original_content = f.read()
24
-
25
-
26
- if create_backup:
27
- backup_path = file_path + ".bak"
28
- with open(backup_path, 'w') as f:
29
- f.write(original_content)
30
-
31
-
32
- prompt = """You are a code editing assistant. Analyze this file and make the requested changes.
20
+
21
+ # Ensure parent directory exists
22
+ os.makedirs(os.path.dirname(file_path) or '.', exist_ok=True)
23
+
24
+ # If file doesn't exist, create it
25
+ if not os.path.exists(file_path):
26
+ prompt = """You are a code writing assistant. Create the content for a new file based on these instructions.
27
+
28
+ Instructions: """ + edit_instructions + """
29
+
30
+ Return a JSON object with:
31
+ 1. "content": The full content to write to the file
32
+ 2. "explanation": Brief explanation of what was created
33
+
34
+ Example response:
35
+ {"content": "print('hello world')", "explanation": "Created a Python hello world script"}
36
+ """
37
+ response = get_llm_response(prompt, model=npc.model, provider=npc.provider, npc=npc, format="json")
38
+ result = response.get("response", {})
39
+ content = result.get("content", edit_instructions)
40
+ explanation = result.get("explanation", "Created new file")
41
+
42
+ with open(file_path, 'w') as f:
43
+ f.write(content)
44
+
45
+ output = "Created " + file_path + "\n\n" + explanation
46
+ else:
47
+ with open(file_path, 'r') as f:
48
+ original_content = f.read()
49
+
50
+ if create_backup:
51
+ backup_path = file_path + ".bak"
52
+ with open(backup_path, 'w') as f:
53
+ f.write(original_content)
54
+
55
+ prompt = """You are a code editing assistant. Analyze this file and make the requested changes.
33
56
 
34
57
  File content:
35
58
  """ + original_content + """
@@ -49,49 +72,48 @@ steps:
49
72
  Example response:
50
73
  {"modifications": [{"type": "replace", "original": "old code", "replacement": "new code"}], "explanation": "Updated the code"}
51
74
  """
52
-
53
- response = get_llm_response(prompt, model=npc.model, provider=npc.provider, npc=npc, format="json")
54
-
55
- result = response.get("response", {})
56
- modifications = result.get("modifications", [])
57
- explanation = result.get("explanation", "No explanation provided")
58
-
59
-
60
- updated_content = original_content
61
- changes_applied = 0
62
-
63
- for mod in modifications:
64
- print(mod)
65
- mod_type = mod.get("type")
66
-
67
- if mod_type == "replace":
68
- original = mod.get("original")
69
- replacement = mod.get("replacement")
70
- if original in updated_content:
71
- updated_content = updated_content.replace(original, replacement)
72
- changes_applied += 1
73
-
74
- elif mod_type == "insert_after":
75
- target = mod.get("target")
76
- insertion = mod.get("insertion")
77
- if target in updated_content:
78
- updated_content = updated_content.replace(target, target + insertion)
79
- changes_applied += 1
80
-
81
- elif mod_type == "insert_before":
82
- target = mod.get("target")
83
- insertion = mod.get("insertion")
84
- if target in updated_content:
85
- updated_content = updated_content.replace(target, insertion + target)
86
- changes_applied += 1
87
-
88
- elif mod_type == "delete":
89
- target = mod.get("target")
90
- if target in updated_content:
91
- updated_content = updated_content.replace(target, "")
92
- changes_applied += 1
93
-
94
- with open(file_path, 'w') as f:
95
- f.write(updated_content)
96
-
97
- output = "Applied " + str(changes_applied) + " changes to " + file_path + "\n\n" + explanation
75
+
76
+ response = get_llm_response(prompt, model=npc.model, provider=npc.provider, npc=npc, format="json")
77
+
78
+ result = response.get("response", {})
79
+ modifications = result.get("modifications", [])
80
+ explanation = result.get("explanation", "No explanation provided")
81
+
82
+ updated_content = original_content
83
+ changes_applied = 0
84
+
85
+ for mod in modifications:
86
+ print(mod)
87
+ mod_type = mod.get("type")
88
+
89
+ if mod_type == "replace":
90
+ original = mod.get("original")
91
+ replacement = mod.get("replacement")
92
+ if original in updated_content:
93
+ updated_content = updated_content.replace(original, replacement)
94
+ changes_applied += 1
95
+
96
+ elif mod_type == "insert_after":
97
+ target = mod.get("target")
98
+ insertion = mod.get("insertion")
99
+ if target in updated_content:
100
+ updated_content = updated_content.replace(target, target + insertion)
101
+ changes_applied += 1
102
+
103
+ elif mod_type == "insert_before":
104
+ target = mod.get("target")
105
+ insertion = mod.get("insertion")
106
+ if target in updated_content:
107
+ updated_content = updated_content.replace(target, insertion + target)
108
+ changes_applied += 1
109
+
110
+ elif mod_type == "delete":
111
+ target = mod.get("target")
112
+ if target in updated_content:
113
+ updated_content = updated_content.replace(target, "")
114
+ changes_applied += 1
115
+
116
+ with open(file_path, 'w') as f:
117
+ f.write(updated_content)
118
+
119
+ output = "Applied " + str(changes_applied) + " changes to " + file_path + "\n\n" + explanation
@@ -33,6 +33,8 @@ steps:
33
33
  from typing import List, Dict, Any, Tuple
34
34
  from pathlib import Path
35
35
 
36
+ import requests as _requests
37
+
36
38
  from npcpy.llm_funcs import get_llm_response
37
39
  from npcpy.npc_compiler import NPC
38
40
 
@@ -65,6 +67,7 @@ steps:
65
67
 
66
68
  model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
67
69
  provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
70
+ _alicanto_directive = (npc.primary_directive if npc and hasattr(npc, 'primary_directive') else "") or ""
68
71
 
69
72
  # ========== Utility ==========
70
73
  def get_size():
@@ -164,8 +167,26 @@ steps:
164
167
  except:
165
168
  return "Error listing directory."
166
169
 
167
- def execute_shell_command(command: str) -> str:
168
- """Execute a shell command and return stdout/stderr."""
170
+ def run_python(code: str) -> str:
171
+ """Execute Python code and return the output. This is your PRIMARY tool for data analysis, file processing, API calls, computations, and any programmatic work. Use this for: downloading data, parsing files, running analyses, making HTTP requests, processing CSVs/FITS/JSON, plotting, statistics, etc. The code runs in a fresh namespace with access to standard library and installed packages (numpy, pandas, astropy, requests, matplotlib, scipy, etc.)."""
172
+ import io as _io
173
+ _old_stdout = sys.stdout
174
+ _old_stderr = sys.stderr
175
+ _capture = _io.StringIO()
176
+ sys.stdout = _capture
177
+ sys.stderr = _capture
178
+ _ns = {'__builtins__': __builtins__}
179
+ try:
180
+ exec(code, _ns)
181
+ except Exception as _e:
182
+ print(f"Error: {type(_e).__name__}: {_e}")
183
+ finally:
184
+ sys.stdout = _old_stdout
185
+ sys.stderr = _old_stderr
186
+ return _capture.getvalue()[:5000] if _capture.getvalue().strip() else "(no output)"
187
+
188
+ def shell_command(command: str) -> str:
189
+ """Execute a shell command. ONLY use this for simple system tasks like installing packages (pip install), checking disk space, or listing system info. For ALL data work, analysis, file processing, HTTP requests, and computation, use run_python instead."""
169
190
  try:
170
191
  result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60)
171
192
  out = ""
@@ -179,12 +200,14 @@ steps:
179
200
  except Exception as e:
180
201
  return f"Error: {e}"
181
202
 
203
+ _search_provider = os.environ.get('NPCSH_SEARCH_PROVIDER', 'perplexity')
204
+
182
205
  def _web_search_tool(query: str) -> str:
183
206
  """Search the web for information."""
184
207
  if not WEB_AVAILABLE:
185
208
  return "Web search not available."
186
209
  try:
187
- results = search_web(query, num_results=5)
210
+ results = search_web(query, num_results=5, provider=_search_provider)
188
211
  if not results:
189
212
  return "No results found."
190
213
  if isinstance(results, list):
@@ -199,6 +222,61 @@ steps:
199
222
  except Exception as e:
200
223
  return f"Search error: {e}"
201
224
 
225
+ _s2_api_key = os.environ.get('S2_API_KEY', '')
226
+
227
+ def search_papers(query: str, limit: int = 10) -> str:
228
+ """Search Semantic Scholar for academic papers. Returns titles, authors, year, citation count, abstracts, and URLs."""
229
+ s2_url = "https://api.semanticscholar.org/graph/v1/paper/search"
230
+ params = {
231
+ "query": query,
232
+ "limit": min(limit, 20),
233
+ "fields": "title,abstract,authors,year,citationCount,url,tldr,venue"
234
+ }
235
+ try:
236
+ # Try with API key first if available
237
+ if _s2_api_key:
238
+ resp = _requests.get(s2_url, headers={"x-api-key": _s2_api_key}, params=params, timeout=30)
239
+ if resp.status_code == 403:
240
+ # Key expired/revoked, fall back to unauthenticated
241
+ resp = _requests.get(s2_url, params=params, timeout=30)
242
+ else:
243
+ resp = _requests.get(s2_url, params=params, timeout=30)
244
+ if resp.status_code == 429:
245
+ # Rate limited, wait and retry once
246
+ time.sleep(1.5)
247
+ resp = _requests.get(s2_url, params=params, timeout=30)
248
+ resp.raise_for_status()
249
+ papers = resp.json().get('data', [])
250
+ if not papers:
251
+ return f"No papers found for: {query}"
252
+ out = []
253
+ for i, p in enumerate(papers, 1):
254
+ title = p.get('title', 'No title')
255
+ year = p.get('year', '?')
256
+ cites = p.get('citationCount', 0)
257
+ authors = ', '.join([a.get('name', '') for a in p.get('authors', [])[:3]])
258
+ if len(p.get('authors', [])) > 3:
259
+ authors += ' et al.'
260
+ tldr = p.get('tldr', {}).get('text', '') if p.get('tldr') else ''
261
+ abstract = (p.get('abstract') or '')[:200]
262
+ paper_url = p.get('url', '')
263
+ venue = p.get('venue', '')
264
+ entry = f"{i}. {title} ({year}) [{cites} citations]"
265
+ entry += f"\n Authors: {authors}"
266
+ if venue:
267
+ entry += f"\n Venue: {venue}"
268
+ if tldr:
269
+ entry += f"\n TL;DR: {tldr}"
270
+ elif abstract:
271
+ entry += f"\n Abstract: {abstract}..."
272
+ entry += f"\n URL: {paper_url}"
273
+ out.append(entry)
274
+ return "\n\n".join(out)
275
+ except _requests.exceptions.RequestException as e:
276
+ return f"Semantic Scholar API error: {e}"
277
+ except Exception as e:
278
+ return f"Paper search error: {e}"
279
+
202
280
  # ========== File Provenance (matching original) ==========
203
281
  @dataclass
204
282
  class FileProvenance:
@@ -362,14 +440,14 @@ steps:
362
440
  except Exception as e:
363
441
  return f"Wander failed: {e}"
364
442
 
365
- tools = [create_file, append_to_file, replace_in_file, read_file,
366
- list_files, execute_shell_command, _web_search_tool, wander_wrapper]
443
+ tools = [run_python, create_file, append_to_file, replace_in_file, read_file,
444
+ list_files, _web_search_tool, search_papers, shell_command, wander_wrapper]
367
445
 
368
446
  agent = NPC(
369
447
  name=agent_name.replace(' ', '_').lower(),
370
448
  model=_model,
371
449
  provider=_provider,
372
- primary_directive=agent_persona,
450
+ primary_directive=_alicanto_directive + "\n\n" + agent_persona,
373
451
  tools=tools
374
452
  )
375
453
 
@@ -379,6 +457,7 @@ steps:
379
457
  created_files = set()
380
458
  summary = {}
381
459
  major_step = 0
460
+ stall_count = 0 # consecutive steps with no filesystem change
382
461
 
383
462
  while major_step < _max_steps:
384
463
  # Check for skip/quit
@@ -401,37 +480,11 @@ steps:
401
480
  history_str = "\n".join(summarized_history)
402
481
  next_step_text = f"This is the next step suggested by your advisor. : BEGIN NEXT_STEP: {summary.get('next_step')} END NEXT STEP" if summary else ""
403
482
 
404
- search_provider = os.environ.get('NPCSH_SEARCH_PROVIDER', 'duckduckgo')
405
- initial_prompt = f"""Test the following hypothesis: '{hypothesis}' as related to the user query: '{user_query}'.
406
- Only focus on your specific hypothesis, other agents are being tasked with other aspects of the problem.
407
-
408
- Use bash commands to carry out research through the execute_shell_command.
409
- Adjust files with `replace_in_file` and use `read_file` and `list_files` to verify file states and file creation.
410
- Create files with create_file()
411
-
412
- Test with execute_shell_command when needed
413
- Get unstuck with wander_wrapper
414
-
415
- When you have a definitive result, say RESEARCH_COMPLETE.
416
-
417
- FILE PROVENANCE HISTORY:
418
- {chr(10).join(provenance_summary)}
419
-
420
- CURRENT FILES: {list(fs_before.keys())}
421
-
422
- COMPLETE ACTION HISTORY:
423
- BEGIN HISTORY
483
+ initial_prompt = f"""Hypothesis: '{hypothesis}'
484
+ Query: '{user_query}'
485
+ Files: {list(fs_before.keys())}
424
486
  {history_str}
425
- END HISTORY
426
-
427
- What specific action will you take next to test your hypothesis?
428
- AVAILABLE TOOLS: create_file, append_to_file, replace_in_file, read_file, list_files, execute_shell_command, wander_wrapper, _web_search_tool.
429
-
430
- Do not repeat actions. Use `_web_search_tool` with provider of {search_provider} to look up items if you are struggling.
431
-
432
- {next_step_text}
433
-
434
- Your goal is to research. To set up experiments, create figures, and produce data outputs in csvs for verification and reproducibility."""
487
+ {next_step_text}"""
435
488
 
436
489
  ui_state['log'].append(f"\033[90m Major step {major_step + 1}\033[0m")
437
490
 
@@ -495,8 +548,16 @@ steps:
495
548
 
496
549
  fs_after = get_filesystem_state()
497
550
  new_files = set(fs_after.keys()) - set(fs_before.keys())
551
+ changed_files = {f for f in fs_after if fs_before.get(f) != fs_after.get(f)}
498
552
  if new_files:
499
553
  ui_state['log'].append(f" \033[32mNew files: {list(new_files)}\033[0m")
554
+ stall_count = 0
555
+ elif changed_files:
556
+ stall_count = 0
557
+ else:
558
+ stall_count += 1
559
+ if stall_count >= 3:
560
+ ui_state['log'].append(f" \033[33mStalled for {stall_count} steps, forcing wrap-up\033[0m")
500
561
 
501
562
  combined_thought = " ".join(all_thoughts)
502
563
  combined_action = " | ".join(filter(None, all_actions))
@@ -619,7 +680,7 @@ steps:
619
680
 
620
681
  Focus ONLY on the {next_section} section. Write 2-4 paragraphs of substantial academic content.
621
682
 
622
- Available tools: replace_in_file, read_file, _web_search_tool"""
683
+ Available tools: replace_in_file, read_file, _web_search_tool, search_papers"""
623
684
 
624
685
  for micro in range(5):
625
686
  if micro == 0:
@@ -743,7 +804,7 @@ steps:
743
804
  Use replace_in_file to make targeted improvements to paper.tex.
744
805
  Use read_file to check current state.
745
806
 
746
- Available tools: replace_in_file, read_file, append_to_file, _web_search_tool"""
807
+ Available tools: replace_in_file, read_file, append_to_file, _web_search_tool, search_papers"""
747
808
 
748
809
  coord_messages = []
749
810
  for micro in range(8):
@@ -965,13 +1026,13 @@ steps:
965
1026
  except Exception as e:
966
1027
  return f"Wander failed: {e}"
967
1028
 
968
- coord_tools = [create_file, append_to_file, replace_in_file, read_file,
969
- list_files, execute_shell_command, _web_search_tool, wander_wrapper_coord]
1029
+ coord_tools = [run_python, create_file, append_to_file, replace_in_file, read_file,
1030
+ list_files, _web_search_tool, search_papers, shell_command, wander_wrapper_coord]
970
1031
 
971
1032
  coordinator = NPC(
972
1033
  name="Alicanto",
973
1034
  model=model, provider=provider,
974
- primary_directive="You are Alicanto the mythical bird. You research topics iteratively by writing to LaTeX files and searching for more information.",
1035
+ primary_directive=_alicanto_directive,
975
1036
  tools=coord_tools
976
1037
  )
977
1038