npcsh 1.1.21__py3-none-any.whl → 1.1.23__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 (188) hide show
  1. npcsh/_state.py +282 -125
  2. npcsh/benchmark/npcsh_agent.py +77 -232
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
  4. npcsh/config.py +5 -2
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +8 -6
  7. npcsh/npc_team/corca.npc +5 -12
  8. npcsh/npc_team/frederic.npc +6 -9
  9. npcsh/npc_team/guac.npc +4 -4
  10. npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
  12. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  13. npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
  14. npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
  15. npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
  16. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
  17. npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
  19. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  20. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
  21. npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
  22. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  23. npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
  24. npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
  25. npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
  26. npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
  27. npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
  28. npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
  29. npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
  30. npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
  31. npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
  32. npcsh/npc_team/kadiefa.npc +6 -6
  33. npcsh/npc_team/npcsh.ctx +16 -0
  34. npcsh/npc_team/plonk.npc +5 -9
  35. npcsh/npc_team/sibiji.npc +15 -7
  36. npcsh/npcsh.py +1 -0
  37. npcsh/routes.py +0 -4
  38. npcsh/yap.py +22 -4
  39. npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
  40. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
  41. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
  42. npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
  43. npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
  44. npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
  45. npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
  46. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
  47. npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
  48. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
  49. npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  50. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
  51. npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
  52. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
  53. npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
  54. npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
  55. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
  56. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
  57. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
  58. npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
  59. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
  60. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
  61. npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
  62. npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
  63. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
  64. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
  65. npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
  66. npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
  67. npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
  68. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
  69. npcsh-1.1.23.dist-info/RECORD +216 -0
  70. npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
  71. npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
  72. npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
  73. npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
  74. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
  75. npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
  76. npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
  77. npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
  78. npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
  79. npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
  80. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
  81. npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
  82. npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
  83. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
  84. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
  85. npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
  86. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
  87. npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
  88. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
  89. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  90. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  91. npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
  92. npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
  93. npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
  94. npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
  95. npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
  96. npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
  97. npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
  98. npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  99. npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
  100. npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
  101. npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
  102. npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
  103. npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
  104. npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
  105. npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
  106. npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
  107. npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
  108. npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
  109. npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
  110. npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
  111. npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
  112. npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
  113. npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
  114. npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
  115. npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
  116. npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
  117. npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
  118. npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
  119. npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
  120. npcsh-1.1.21.dist-info/RECORD +0 -243
  121. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  122. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  123. /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
  124. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
  125. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  126. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  127. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  128. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  129. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
  130. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
  131. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  132. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  133. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
  134. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
  135. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
  136. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
  137. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  138. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  139. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
  140. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
  141. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
  142. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
  143. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  144. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  145. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  146. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  147. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  148. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
  149. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
  150. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  151. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
  152. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  153. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
  154. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
  155. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
  156. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
  157. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  158. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
  159. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
  160. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  161. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
  162. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  163. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
  164. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
  165. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
  166. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
  167. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  168. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
  169. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
  170. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
  171. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
  172. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
  173. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
  174. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
  175. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  181. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
  182. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
  183. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  184. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
  185. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
  186. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
  187. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
  188. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -0,0 +1,378 @@
1
+ jinx_name: build
2
+ description: Interactive TUI for building deployment artifacts from an NPC team
3
+ interactive: true
4
+ inputs:
5
+ - target: ""
6
+ - outdir: "./build"
7
+ - team: "./npc_team"
8
+ - port: 5337
9
+ - cors: ""
10
+ steps:
11
+ - name: build
12
+ engine: python
13
+ code: |
14
+ import os
15
+ import sys
16
+ import tty
17
+ import termios
18
+ import select as _sel
19
+
20
+ def _resolve_team_path(raw_team):
21
+ """Resolve team path: try given path first, then local ./npc_team, then global ~/.npcsh/npc_team."""
22
+ candidates = []
23
+ if raw_team:
24
+ candidates.append(os.path.abspath(os.path.expanduser(raw_team)))
25
+ local = os.path.abspath('./npc_team')
26
+ global_ = os.path.expanduser('~/.npcsh/npc_team')
27
+ if local not in candidates:
28
+ candidates.append(local)
29
+ if global_ not in candidates:
30
+ candidates.append(global_)
31
+ for p in candidates:
32
+ if os.path.isdir(p):
33
+ if p != candidates[0] and candidates[0] != p:
34
+ print(f"\033[33m⚠ Local team not found at {candidates[0]}, using {p}\033[0m")
35
+ return p
36
+ raise FileNotFoundError(
37
+ f"No npc_team directory found. Searched:\n"
38
+ + "\n".join(f" - {c}" for c in candidates)
39
+ + "\n\nCreate a local npc_team/ or ensure ~/.npcsh/npc_team exists."
40
+ )
41
+
42
+ _direct_target = (context.get('target') or '').strip().lower()
43
+ _direct = bool(_direct_target) or not sys.stdin.isatty()
44
+
45
+ if _direct:
46
+ # Direct build: target passed explicitly or non-interactive
47
+ try:
48
+ from npcpy.build_funcs import (
49
+ build_flask_server as _bf,
50
+ build_docker_compose as _bd,
51
+ build_cli_executable as _bc,
52
+ build_static_site as _bs,
53
+ )
54
+ _target = _direct_target or 'flask'
55
+ _builders = {'flask': _bf, 'docker': _bd, 'cli': _bc, 'static': _bs}
56
+ if _target not in _builders:
57
+ context['output'] = f"Unknown target: {_target}. Available: {list(_builders.keys())}"
58
+ else:
59
+ _cfg = {
60
+ 'team_path': _resolve_team_path(context.get('team')),
61
+ 'output_dir': os.path.abspath(os.path.expanduser(context.get('outdir') or './build')),
62
+ 'target': _target,
63
+ 'port': int(context.get('port') or 5337),
64
+ 'cors_origins': [c.strip() for c in (context.get('cors') or '').split(',') if c.strip()] or None,
65
+ }
66
+ _r = _builders[_target](_cfg)
67
+ context['output'] = _r.get('output', 'Build complete.')
68
+ except ImportError:
69
+ context['output'] = "Build functions not available. Install npcpy with build support."
70
+ except Exception as _e:
71
+ context['output'] = f"Build failed: {_e}"
72
+ context['messages'] = context.get('messages', [])
73
+ exit()
74
+
75
+ try:
76
+ from npcpy.build_funcs import (
77
+ build_flask_server,
78
+ build_docker_compose,
79
+ build_cli_executable,
80
+ build_static_site,
81
+ )
82
+ BUILD_AVAILABLE = True
83
+ except ImportError:
84
+ BUILD_AVAILABLE = False
85
+
86
+ if not BUILD_AVAILABLE:
87
+ context['output'] = "Build functions not available. Install npcpy with build support."
88
+ exit()
89
+
90
+ # ========== State ==========
91
+ class BuildState:
92
+ def __init__(self):
93
+ self.phase = 0 # 0=select target, 1=configure, 2=building, 3=result
94
+ self.sel = 0
95
+ self.targets = [
96
+ {'key': 'flask', 'name': 'Flask Server', 'desc': 'Standalone Python web server with NPC API endpoints'},
97
+ {'key': 'docker', 'name': 'Docker', 'desc': 'Containerized deployment with Dockerfile and docker-compose'},
98
+ {'key': 'cli', 'name': 'CLI Scripts', 'desc': 'Per-NPC executable scripts for direct CLI usage'},
99
+ {'key': 'static', 'name': 'Static Site', 'desc': 'HTML documentation page listing team NPCs'},
100
+ ]
101
+ try:
102
+ _resolved_team = _resolve_team_path(context.get('team'))
103
+ except FileNotFoundError:
104
+ _resolved_team = os.path.expanduser(context.get('team') or './npc_team')
105
+ self.config = {
106
+ 'outdir': os.path.expanduser(context.get('outdir') or './build'),
107
+ 'team': _resolved_team,
108
+ 'port': str(context.get('port') or 5337),
109
+ 'cors': context.get('cors') or '',
110
+ }
111
+ self.config_keys = ['outdir', 'team', 'port', 'cors']
112
+ self.config_labels = {'outdir': 'Output Dir', 'team': 'Team Path', 'port': 'Port', 'cors': 'CORS Origins'}
113
+ self.config_sel = 0
114
+ self.editing = False
115
+ self.edit_buf = ""
116
+ self.edit_key = ""
117
+ self.result = ""
118
+ self.error = ""
119
+ self.files_created = []
120
+
121
+ ui = BuildState()
122
+
123
+ # TUI mode: no target was passed, user picks interactively
124
+
125
+ # ========== Helpers ==========
126
+ def get_size():
127
+ try:
128
+ s = os.get_terminal_size()
129
+ return s.columns, s.lines
130
+ except:
131
+ return 80, 24
132
+
133
+ # ========== Rendering ==========
134
+ def render():
135
+ width, height = get_size()
136
+ out = []
137
+ out.append('\033[2J\033[H')
138
+
139
+ phase_names = ['Select Target', 'Configure', 'Building...', 'Complete']
140
+ header = f' NPC BUILD - {phase_names[ui.phase]} '
141
+ out.append(f'\033[1;1H\033[7;1m{header.ljust(width)}\033[0m')
142
+
143
+ if ui.phase == 0:
144
+ render_targets(out, width, height)
145
+ elif ui.phase == 1:
146
+ render_config(out, width, height)
147
+ elif ui.phase == 2:
148
+ render_building(out, width, height)
149
+ elif ui.phase == 3:
150
+ render_result(out, width, height)
151
+
152
+ if ui.error:
153
+ out.append(f'\033[{height-1};1H\033[K \033[31m{ui.error[:width-3]}\033[0m')
154
+
155
+ sys.stdout.write(''.join(out))
156
+ sys.stdout.flush()
157
+
158
+ def render_targets(out, width, height):
159
+ banner = [
160
+ '\033[33m ██████╗ ██╗ ██╗██╗██╗ ██████╗ \033[0m',
161
+ '\033[33m██╔══██╗██║ ██║██║██║ ██╔══██╗\033[0m',
162
+ '\033[33m██████╔╝██║ ██║██║██║ ██║ ██║\033[0m',
163
+ '\033[33m██╔══██╗██║ ██║██║██║ ██║ ██║\033[0m',
164
+ '\033[33m██████╔╝╚██████╔╝██║███████╗██████╔╝\033[0m',
165
+ '\033[33m╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ \033[0m',
166
+ ]
167
+ for i, line in enumerate(banner):
168
+ out.append(f'\033[{3+i};3H{line}')
169
+
170
+ y = 3 + len(banner) + 1
171
+ out.append(f'\033[{y};3H\033[1mSelect a build target:\033[0m')
172
+ y += 2
173
+
174
+ for i, target in enumerate(ui.targets):
175
+ selected = (i == ui.sel)
176
+ if selected:
177
+ out.append(f'\033[{y};2H\033[7;1m > {target["name"]:<20}\033[0m \033[7m{target["desc"][:width-28]}\033[0m')
178
+ else:
179
+ out.append(f'\033[{y};2H \033[1m{target["name"]:<20}\033[0m \033[90m{target["desc"][:width-28]}\033[0m')
180
+ y += 2
181
+
182
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Navigate Enter:Select q:Quit \033[0m'.ljust(width))
183
+
184
+ def render_config(out, width, height):
185
+ target = ui.targets[ui.sel]
186
+ out.append(f'\033[3;3H\033[1mTarget: \033[36m{target["name"]}\033[0m')
187
+ out.append(f'\033[4;3H\033[90m{target["desc"]}\033[0m')
188
+
189
+ out.append(f'\033[6;3H\033[1mConfiguration:\033[0m')
190
+
191
+ y = 8
192
+ for i, key in enumerate(ui.config_keys):
193
+ label = ui.config_labels[key]
194
+ val = ui.config[key]
195
+ selected = (i == ui.config_sel)
196
+
197
+ if ui.editing and key == ui.edit_key:
198
+ out.append(f'\033[{y};3H\033[33m{label}:\033[0m \033[7m {ui.edit_buf}_ \033[0m')
199
+ elif selected:
200
+ out.append(f'\033[{y};3H\033[7m {label}: {val} \033[0m')
201
+ else:
202
+ out.append(f'\033[{y};3H \033[1m{label}:\033[0m {val}')
203
+ y += 2
204
+
205
+ # Show which configs are relevant for this target
206
+ y += 1
207
+ relevant = {'flask': ['outdir', 'team', 'port', 'cors'],
208
+ 'docker': ['outdir', 'team', 'port', 'cors'],
209
+ 'cli': ['outdir', 'team'],
210
+ 'static': ['outdir', 'team']}
211
+ rel = relevant.get(target['key'], ui.config_keys)
212
+ out.append(f'\033[{y};3H\033[90mRelevant for {target["name"]}: {", ".join(rel)}\033[0m')
213
+
214
+ if ui.editing:
215
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:Save Esc:Cancel \033[0m'.ljust(width))
216
+ else:
217
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Navigate e:Edit Enter:Build Backspace:Back q:Quit \033[0m'.ljust(width))
218
+
219
+ def render_building(out, width, height):
220
+ target = ui.targets[ui.sel]
221
+ mid = height // 2
222
+ out.append(f'\033[{mid};{width//2-10}H\033[33;1mBuilding {target["name"]}...\033[0m')
223
+
224
+ def render_result(out, width, height):
225
+ target = ui.targets[ui.sel]
226
+ y = 3
227
+ if ui.error:
228
+ out.append(f'\033[{y};3H\033[31;1mBuild Failed\033[0m')
229
+ y += 2
230
+ out.append(f'\033[{y};3H\033[31m{ui.error[:width-6]}\033[0m')
231
+ else:
232
+ out.append(f'\033[{y};3H\033[32;1mBuild Complete: {target["name"]}\033[0m')
233
+ y += 2
234
+ for line in ui.result.split('\n'):
235
+ if y >= height - 3:
236
+ break
237
+ out.append(f'\033[{y};3H{line[:width-6]}')
238
+ y += 1
239
+
240
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:New Build o:Open output dir q:Quit \033[0m'.ljust(width))
241
+
242
+ # ========== Build Execution ==========
243
+ def do_build():
244
+ target = ui.targets[ui.sel]
245
+ config = {
246
+ 'team_path': _resolve_team_path(ui.config['team']),
247
+ 'output_dir': os.path.abspath(os.path.expanduser(ui.config['outdir'])),
248
+ 'target': target['key'],
249
+ 'port': int(ui.config['port']),
250
+ 'cors_origins': [c.strip() for c in ui.config['cors'].split(',') if c.strip()] or None,
251
+ }
252
+
253
+ builders = {
254
+ 'flask': build_flask_server,
255
+ 'docker': build_docker_compose,
256
+ 'cli': build_cli_executable,
257
+ 'static': build_static_site,
258
+ }
259
+
260
+ try:
261
+ result = builders[target['key']](config)
262
+ ui.result = result.get('output', 'Build complete.')
263
+ ui.error = ""
264
+ except Exception as e:
265
+ ui.error = str(e)
266
+ ui.result = ""
267
+
268
+ ui.phase = 3
269
+
270
+ # ========== Input ==========
271
+ def handle_input(c, fd):
272
+ if ui.editing:
273
+ return handle_edit(c, fd)
274
+
275
+ if c == '\x1b':
276
+ if _sel.select([fd], [], [], 0.05)[0]:
277
+ c2 = os.read(fd, 1).decode('latin-1')
278
+ if c2 == '[':
279
+ c3 = os.read(fd, 1).decode('latin-1')
280
+ if c3 == 'A': move_up()
281
+ elif c3 == 'B': move_down()
282
+ return True
283
+
284
+ if c == 'q' or c == '\x03':
285
+ return False
286
+
287
+ if ui.phase == 0:
288
+ if c == 'j': move_down()
289
+ elif c == 'k': move_up()
290
+ elif c in ('\r', '\n'):
291
+ ui.phase = 1
292
+ ui.config_sel = 0
293
+ elif ui.phase == 1:
294
+ if c == 'j': move_down()
295
+ elif c == 'k': move_up()
296
+ elif c == 'e':
297
+ key = ui.config_keys[ui.config_sel]
298
+ ui.editing = True
299
+ ui.edit_key = key
300
+ ui.edit_buf = ui.config[key]
301
+ elif c == '\x7f' or c == '\x08':
302
+ ui.phase = 0
303
+ ui.config_sel = 0
304
+ elif c in ('\r', '\n'):
305
+ ui.phase = 2
306
+ render()
307
+ do_build()
308
+ elif ui.phase == 3:
309
+ if c in ('\r', '\n'):
310
+ ui.phase = 0
311
+ ui.sel = 0
312
+ ui.error = ""
313
+ ui.result = ""
314
+ elif c == 'o':
315
+ outdir = os.path.abspath(os.path.expanduser(ui.config['outdir']))
316
+ if os.path.isdir(outdir):
317
+ import subprocess
318
+ try:
319
+ subprocess.Popen(['xdg-open', outdir], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
320
+ except:
321
+ pass
322
+ return True
323
+
324
+ def handle_edit(c, fd):
325
+ if c == '\x1b':
326
+ if _sel.select([fd], [], [], 0.05)[0]:
327
+ os.read(fd, 2)
328
+ ui.editing = False
329
+ ui.edit_buf = ""
330
+ return True
331
+ if c in ('\r', '\n'):
332
+ ui.config[ui.edit_key] = ui.edit_buf
333
+ ui.editing = False
334
+ return True
335
+ if c == '\x7f' or c == '\x08':
336
+ ui.edit_buf = ui.edit_buf[:-1]
337
+ return True
338
+ if c >= ' ' and c <= '~':
339
+ ui.edit_buf += c
340
+ return True
341
+
342
+ def move_up():
343
+ if ui.phase == 0:
344
+ ui.sel = max(0, ui.sel - 1)
345
+ elif ui.phase == 1:
346
+ ui.config_sel = max(0, ui.config_sel - 1)
347
+
348
+ def move_down():
349
+ if ui.phase == 0:
350
+ ui.sel = min(len(ui.targets) - 1, ui.sel + 1)
351
+ elif ui.phase == 1:
352
+ ui.config_sel = min(len(ui.config_keys) - 1, ui.config_sel + 1)
353
+
354
+ # ========== Main Loop ==========
355
+ fd = sys.stdin.fileno()
356
+ old_settings = termios.tcgetattr(fd)
357
+
358
+ try:
359
+ tty.setcbreak(fd)
360
+ sys.stdout.write('\033[?25l')
361
+ render()
362
+
363
+ running = True
364
+ while running:
365
+ if _sel.select([fd], [], [], 0.5)[0]:
366
+ c = os.read(fd, 1).decode('latin-1')
367
+ running = handle_input(c, fd)
368
+ render()
369
+ finally:
370
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
371
+ sys.stdout.write('\033[?25h\033[2J\033[H')
372
+ sys.stdout.flush()
373
+
374
+ if ui.result:
375
+ print(ui.result)
376
+
377
+ context['output'] = ui.result or 'Build cancelled.'
378
+ context['messages'] = context.get('messages', [])
@@ -1,4 +1,4 @@
1
- jinx_name: config_tui
1
+ jinx_name: config
2
2
  description: Interactive TUI editor for npcsh configuration (~/.npcshrc)
3
3
  interactive: true
4
4
  inputs: []