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.
- npcsh/_state.py +282 -125
- npcsh/benchmark/npcsh_agent.py +77 -232
- npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
- npcsh/config.py +5 -2
- npcsh/mcp_server.py +9 -1
- npcsh/npc_team/alicanto.npc +8 -6
- npcsh/npc_team/corca.npc +5 -12
- npcsh/npc_team/frederic.npc +6 -9
- npcsh/npc_team/guac.npc +4 -4
- npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
- npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
- npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
- npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
- npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
- npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
- npcsh/npc_team/jinxs/modes/build.jinx +378 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
- npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
- npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
- npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
- npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
- npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
- npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
- npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
- npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
- npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
- npcsh/npc_team/kadiefa.npc +6 -6
- npcsh/npc_team/npcsh.ctx +16 -0
- npcsh/npc_team/plonk.npc +5 -9
- npcsh/npc_team/sibiji.npc +15 -7
- npcsh/npcsh.py +1 -0
- npcsh/routes.py +0 -4
- npcsh/yap.py +22 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
- npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
- npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
- npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
- npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
- npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
- npcsh-1.1.23.dist-info/RECORD +216 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
- npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
- npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
- npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
- npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
- npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
- npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
- npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
- npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
- npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
- npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
- npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
- npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
- npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
- npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
- npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
- npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
- npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
- npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
- npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
- npcsh-1.1.21.dist-info/RECORD +0 -243
- /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
- /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
- /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
168
|
-
"""Execute
|
|
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,
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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,
|
|
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=
|
|
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', [])
|