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
@@ -18,4 +18,5 @@ primary_directive: |
18
18
  Use web search and browsing tools to discover new information.
19
19
  jinxs:
20
20
  - lib/core/python
21
- - bin/wander
21
+ - lib/core/sh
22
+ - lib/core/search/web_search
npcsh/npc_team/sibiji.npc CHANGED
@@ -17,8 +17,8 @@ primary_directive: |
17
17
  When delegating, match the task to the agent whose primary_directive best fits. Basic search inquiries can be handled by yourself. Do not delegate unnecessarily.
18
18
  You have access to the delegate tool to pass tasks to other agents.
19
19
  jinxs:
20
- - lib/orchestration/delegate
21
- - lib/orchestration/convene
20
+ - lib/core/delegate
21
+ - lib/core/convene
22
22
  - lib/core/sh
23
23
  - lib/core/python
24
- - lib/core/search
24
+ - lib/core/search/web_search
@@ -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
 
@@ -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
@@ -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', [])