npcsh 1.1.19__tar.gz → 1.1.20__tar.gz
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-1.1.19/npcsh.egg-info → npcsh-1.1.20}/PKG-INFO +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/_state.py +11 -7
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/config_tui.jinx +3 -2
- npcsh-1.1.20/npcsh/npc_team/jinxs/bin/jinxs.jinx +407 -0
- npcsh-1.1.20/npcsh/npc_team/jinxs/bin/kg.jinx +941 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/memories.jinx +3 -2
- npcsh-1.1.20/npcsh/npc_team/jinxs/bin/models.jinx +343 -0
- npcsh-1.1.20/npcsh/npc_team/jinxs/bin/nql.jinx +471 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/setup.jinx +2 -1
- npcsh-1.1.20/npcsh/npc_team/jinxs/bin/team.jinx +504 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/research/paper_search.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/alicanto.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/arxiv.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/corca.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/plonk.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/reattach.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/spool.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/wander.jinx +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/routes.py +8 -2
- {npcsh-1.1.19 → npcsh-1.1.20/npcsh.egg-info}/PKG-INFO +1 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/SOURCES.txt +4 -2
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/entry_points.txt +4 -1
- {npcsh-1.1.19 → npcsh-1.1.20}/setup.py +1 -1
- npcsh-1.1.19/npcsh/npc_team/jinxs/bin/nql.jinx +0 -141
- npcsh-1.1.19/npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
- npcsh-1.1.19/npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -331
- {npcsh-1.1.19 → npcsh-1.1.20}/LICENSE +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/MANIFEST.in +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/README.md +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/__init__.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/alicanto.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/benchmark/__init__.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/benchmark/npcsh_agent.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/benchmark/runner.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/benchmark/templates/install-npcsh.sh.j2 +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/build.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/completion.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/config.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/conversation_viewer.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/corca.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/diff_viewer.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/execution.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/guac.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/mcp_helpers.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/mcp_server.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/benchmark.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/roll.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/sample.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/sync.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/incognide.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/notify.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/browser/close_browser.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/click.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/chat.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/cmd.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/compress.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/edit_file.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/load_file.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/ots.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/paste.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/python.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/sh.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/sleep.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/sql.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/compile.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/help.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/init.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/serve.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/set.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/shh.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/switch.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/switches.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/usage.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/verbose.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/yap.jinx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npcsh.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/parsing.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/plonk.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/pti.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/spool.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/ui.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/wander.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/yap.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/dependency_links.txt +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/requires.txt +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/top_level.txt +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/project/__init__.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/setup.cfg +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/tests/test_config.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/tests/test_jinxs.py +0 -0
- {npcsh-1.1.19 → npcsh-1.1.20}/tests/test_tool_routing.py +0 -0
|
@@ -2760,15 +2760,19 @@ def process_pipeline_command(
|
|
|
2760
2760
|
if cmd_to_process.startswith("/"):
|
|
2761
2761
|
command_name = cmd_to_process.split()[0].lstrip('/')
|
|
2762
2762
|
|
|
2763
|
-
# Check if this is an interactive mode
|
|
2763
|
+
# Check if this is an interactive mode
|
|
2764
2764
|
is_interactive_mode = False
|
|
2765
|
-
|
|
2766
|
-
# Check
|
|
2767
|
-
|
|
2768
|
-
if os.path.exists(global_modes_jinx):
|
|
2765
|
+
|
|
2766
|
+
# Check if the jinx declares interactive: true
|
|
2767
|
+
if router.is_interactive(command_name):
|
|
2769
2768
|
is_interactive_mode = True
|
|
2770
|
-
|
|
2771
|
-
#
|
|
2769
|
+
|
|
2770
|
+
# Also check modes/ directory (legacy)
|
|
2771
|
+
if not is_interactive_mode:
|
|
2772
|
+
global_modes_jinx = os.path.expanduser(f'~/.npcsh/npc_team/jinxs/modes/{command_name}.jinx')
|
|
2773
|
+
if os.path.exists(global_modes_jinx):
|
|
2774
|
+
is_interactive_mode = True
|
|
2775
|
+
|
|
2772
2776
|
if not is_interactive_mode and state.team and state.team.team_path:
|
|
2773
2777
|
team_modes_jinx = os.path.join(state.team.team_path, 'jinxs', 'modes', f'{command_name}.jinx')
|
|
2774
2778
|
if os.path.exists(team_modes_jinx):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
jinx_name: config_tui
|
|
2
2
|
description: Interactive TUI editor for npcsh configuration (~/.npcshrc)
|
|
3
|
+
interactive: true
|
|
3
4
|
inputs: []
|
|
4
5
|
steps:
|
|
5
6
|
- name: config_editor
|
|
@@ -118,14 +119,14 @@ steps:
|
|
|
118
119
|
if idx == state.selected_idx:
|
|
119
120
|
if state.editing:
|
|
120
121
|
# Show edit mode
|
|
121
|
-
out.append(f"\033[{row};2H\033[
|
|
122
|
+
out.append(f"\033[{row};2H\033[7m{item['label']:<{label_width}}\033[0m")
|
|
122
123
|
# Edit buffer with cursor
|
|
123
124
|
cursor_pos = min(state.edit_cursor, len(state.edit_buffer))
|
|
124
125
|
before = state.edit_buffer[:cursor_pos]
|
|
125
126
|
after = state.edit_buffer[cursor_pos:]
|
|
126
127
|
out.append(f"\033[{row};{label_width+4}H{before}\033[7m \033[0m{after}")
|
|
127
128
|
else:
|
|
128
|
-
out.append(f"\033[{row};2H\033[
|
|
129
|
+
out.append(f"\033[{row};2H\033[7m{mod_indicator}{item['label']:<{label_width}} {display_value[:value_width]}\033[0m")
|
|
129
130
|
else:
|
|
130
131
|
out.append(f"\033[{row};2H{mod_indicator}{item['label']:<{label_width}} {display_value[:value_width]}")
|
|
131
132
|
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
jinx_name: jinxs
|
|
2
|
+
description: Interactive jinx browser - browse, search, and preview available jinxs
|
|
3
|
+
interactive: true
|
|
4
|
+
inputs: []
|
|
5
|
+
steps:
|
|
6
|
+
- name: jinxs_browser
|
|
7
|
+
engine: python
|
|
8
|
+
code: |
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tty
|
|
12
|
+
import termios
|
|
13
|
+
import select
|
|
14
|
+
import yaml
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
if not sys.stdin.isatty():
|
|
18
|
+
context['output'] = "Jinxs browser requires an interactive terminal."
|
|
19
|
+
|
|
20
|
+
elif not state or not state.team:
|
|
21
|
+
context['output'] = "No team loaded."
|
|
22
|
+
|
|
23
|
+
else:
|
|
24
|
+
# ── data loading ─────────────────────────────────────
|
|
25
|
+
def load_jinxs():
|
|
26
|
+
"""Load jinxs from team directory and global ~/.npcsh directory."""
|
|
27
|
+
items = []
|
|
28
|
+
seen_paths = set()
|
|
29
|
+
|
|
30
|
+
def scan_dir(base_dir, source_label):
|
|
31
|
+
if not base_dir.exists():
|
|
32
|
+
return
|
|
33
|
+
for sub in sorted(base_dir.iterdir()):
|
|
34
|
+
if sub.is_dir():
|
|
35
|
+
for jf in sorted(sub.glob('*.jinx')):
|
|
36
|
+
rp = str(jf.resolve())
|
|
37
|
+
if rp in seen_paths:
|
|
38
|
+
continue
|
|
39
|
+
seen_paths.add(rp)
|
|
40
|
+
try:
|
|
41
|
+
with open(jf) as f:
|
|
42
|
+
content = f.read()
|
|
43
|
+
header = content.split('steps:')[0] if 'steps:' in content else content
|
|
44
|
+
data = yaml.safe_load(header) or {}
|
|
45
|
+
except Exception:
|
|
46
|
+
data = {}
|
|
47
|
+
items.append({
|
|
48
|
+
'name': data.get('jinx_name', jf.stem),
|
|
49
|
+
'folder': sub.name,
|
|
50
|
+
'description': data.get('description', ''),
|
|
51
|
+
'inputs': data.get('inputs', []),
|
|
52
|
+
'interactive': data.get('interactive', False),
|
|
53
|
+
'path': str(jf),
|
|
54
|
+
'source': source_label,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# Team jinxs
|
|
58
|
+
team_dir = Path(state.team.team_path) / 'jinxs'
|
|
59
|
+
scan_dir(team_dir, 'team')
|
|
60
|
+
|
|
61
|
+
# Global jinxs
|
|
62
|
+
global_dir = Path.home() / '.npcsh' / 'npc_team' / 'jinxs'
|
|
63
|
+
if global_dir.resolve() != team_dir.resolve():
|
|
64
|
+
scan_dir(global_dir, 'global')
|
|
65
|
+
|
|
66
|
+
return items
|
|
67
|
+
|
|
68
|
+
# ── TUI state ────────────────────────────────────────
|
|
69
|
+
class TUIState:
|
|
70
|
+
def __init__(self):
|
|
71
|
+
self.tab = 0
|
|
72
|
+
self.tabs = ['All', 'Bin', 'Modes', 'Interactive']
|
|
73
|
+
self.sel = 0
|
|
74
|
+
self.scroll = 0
|
|
75
|
+
self.all_jinxs = []
|
|
76
|
+
self.filtered = []
|
|
77
|
+
self.search_mode = False
|
|
78
|
+
self.search_buf = ""
|
|
79
|
+
self.search_query = ""
|
|
80
|
+
self.detail = False
|
|
81
|
+
self.detail_scroll = 0
|
|
82
|
+
self.status = ""
|
|
83
|
+
|
|
84
|
+
ui = TUIState()
|
|
85
|
+
|
|
86
|
+
def term_size():
|
|
87
|
+
try:
|
|
88
|
+
s = os.get_terminal_size()
|
|
89
|
+
return s.columns, s.lines
|
|
90
|
+
except Exception:
|
|
91
|
+
return 80, 24
|
|
92
|
+
|
|
93
|
+
def apply_filters():
|
|
94
|
+
"""Apply tab filter and search query to the full jinx list."""
|
|
95
|
+
items = ui.all_jinxs
|
|
96
|
+
|
|
97
|
+
# Tab filter
|
|
98
|
+
if ui.tab == 1:
|
|
99
|
+
items = [j for j in items if j['folder'] == 'bin']
|
|
100
|
+
elif ui.tab == 2:
|
|
101
|
+
items = [j for j in items if j['folder'] == 'modes']
|
|
102
|
+
elif ui.tab == 3:
|
|
103
|
+
items = [j for j in items if j['interactive']]
|
|
104
|
+
|
|
105
|
+
# Search filter
|
|
106
|
+
if ui.search_query:
|
|
107
|
+
q = ui.search_query.lower()
|
|
108
|
+
items = [j for j in items if q in j['name'].lower() or q in j['description'].lower()]
|
|
109
|
+
|
|
110
|
+
ui.filtered = items
|
|
111
|
+
# Clamp selection
|
|
112
|
+
if ui.sel >= len(ui.filtered):
|
|
113
|
+
ui.sel = max(0, len(ui.filtered) - 1)
|
|
114
|
+
if ui.scroll > ui.sel:
|
|
115
|
+
ui.scroll = ui.sel
|
|
116
|
+
|
|
117
|
+
# ── rendering ────────────────────────────────────────
|
|
118
|
+
def wline(row, text):
|
|
119
|
+
"""Write full line at row, clear to EOL."""
|
|
120
|
+
return f"\033[{row};1H\033[K{text}"
|
|
121
|
+
|
|
122
|
+
def render():
|
|
123
|
+
W, H = term_size()
|
|
124
|
+
out = []
|
|
125
|
+
|
|
126
|
+
# Home cursor (no full-screen clear)
|
|
127
|
+
out.append("\033[H")
|
|
128
|
+
|
|
129
|
+
# ── header ──
|
|
130
|
+
hdr = " Jinxs "
|
|
131
|
+
pad = '=' * W
|
|
132
|
+
out.append(wline(1, f"\033[44;37;1m{pad}\033[0m"))
|
|
133
|
+
out.append(f"\033[1;{max(1, (W - len(hdr)) // 2)}H\033[44;37;1m{hdr}\033[0m")
|
|
134
|
+
|
|
135
|
+
# ── tabs ──
|
|
136
|
+
tb = ""
|
|
137
|
+
for i, t in enumerate(ui.tabs):
|
|
138
|
+
if i == ui.tab:
|
|
139
|
+
tb += f"\033[7;1m [{t}] \033[0m"
|
|
140
|
+
else:
|
|
141
|
+
tb += f" {t} "
|
|
142
|
+
out.append(wline(2, f" {tb}"))
|
|
143
|
+
|
|
144
|
+
# ── count line ──
|
|
145
|
+
count = len(ui.filtered)
|
|
146
|
+
total = len(ui.all_jinxs)
|
|
147
|
+
if ui.search_query:
|
|
148
|
+
count_text = f" {count} matching (of {total}) | search: \"{ui.search_query}\""
|
|
149
|
+
elif ui.tab == 0:
|
|
150
|
+
count_text = f" {total} jinxs loaded"
|
|
151
|
+
else:
|
|
152
|
+
count_text = f" {count} jinxs ({ui.tabs[ui.tab].lower()})"
|
|
153
|
+
out.append(wline(3, f"\033[90m{'─' * W}\033[0m"))
|
|
154
|
+
out.append(wline(4, count_text))
|
|
155
|
+
out.append(wline(5, f"\033[90m{'─' * W}\033[0m"))
|
|
156
|
+
|
|
157
|
+
# ── body ──
|
|
158
|
+
body_start = 6
|
|
159
|
+
body_end = H - 3 # leave room for separator, status, footer
|
|
160
|
+
body_h = body_end - body_start + 1
|
|
161
|
+
if body_h < 1:
|
|
162
|
+
body_h = 1
|
|
163
|
+
|
|
164
|
+
if ui.detail:
|
|
165
|
+
render_detail(out, W, body_start, body_h)
|
|
166
|
+
else:
|
|
167
|
+
render_list(out, W, body_start, body_h)
|
|
168
|
+
|
|
169
|
+
# ── separator ──
|
|
170
|
+
out.append(wline(H - 2, f"\033[90m{'─' * W}\033[0m"))
|
|
171
|
+
|
|
172
|
+
# ── status / search ──
|
|
173
|
+
if ui.search_mode:
|
|
174
|
+
out.append(wline(H - 1, f" \033[33m/\033[0m\033[1m{ui.search_buf}\033[0m\033[90m_\033[0m"))
|
|
175
|
+
elif ui.status:
|
|
176
|
+
out.append(wline(H - 1, f" \033[33m{ui.status[:W-2]}\033[0m"))
|
|
177
|
+
else:
|
|
178
|
+
out.append(wline(H - 1, ""))
|
|
179
|
+
|
|
180
|
+
# ── footer ──
|
|
181
|
+
if ui.search_mode:
|
|
182
|
+
foot = " [Enter] Apply [Esc] Cancel"
|
|
183
|
+
elif ui.detail:
|
|
184
|
+
foot = " [j/k] Scroll [q/Esc] Back"
|
|
185
|
+
else:
|
|
186
|
+
foot = " [Tab] Filter [j/k] Nav [Enter] Detail [/] Search [q] Quit"
|
|
187
|
+
out.append(wline(H, f"\033[44;37m{foot[:W].ljust(W)}\033[0m"))
|
|
188
|
+
|
|
189
|
+
sys.stdout.write(''.join(out))
|
|
190
|
+
sys.stdout.flush()
|
|
191
|
+
|
|
192
|
+
def render_list(out, W, start, body_h):
|
|
193
|
+
vis = ui.filtered[ui.scroll:ui.scroll + body_h]
|
|
194
|
+
name_col = 22 # width for folder/name column
|
|
195
|
+
marker_col = 6 # width for [i] marker
|
|
196
|
+
for r in range(body_h):
|
|
197
|
+
row = start + r
|
|
198
|
+
idx = r + ui.scroll
|
|
199
|
+
if r >= len(vis):
|
|
200
|
+
out.append(wline(row, ""))
|
|
201
|
+
continue
|
|
202
|
+
j = vis[r]
|
|
203
|
+
label = f"{j['folder']}/{j['name']}"
|
|
204
|
+
if len(label) > name_col:
|
|
205
|
+
label = label[:name_col - 1] + "\u2026"
|
|
206
|
+
marker = " [i] " if j['interactive'] else " "
|
|
207
|
+
desc_max = W - name_col - marker_col - 6
|
|
208
|
+
desc = j['description']
|
|
209
|
+
if len(desc) > desc_max:
|
|
210
|
+
desc = desc[:desc_max - 3] + "..."
|
|
211
|
+
if idx == ui.sel:
|
|
212
|
+
line = f" > {label:<{name_col}} {marker} {desc}"
|
|
213
|
+
out.append(wline(row, f"\033[7m{line[:W].ljust(W)}\033[0m"))
|
|
214
|
+
else:
|
|
215
|
+
out.append(wline(row, f" {label:<{name_col}} \033[36m{marker}\033[0m\033[90m{desc}\033[0m"))
|
|
216
|
+
if not ui.filtered:
|
|
217
|
+
out.append(wline(start, " \033[90mNo jinxs match the current filter.\033[0m"))
|
|
218
|
+
for r in range(1, body_h):
|
|
219
|
+
out.append(wline(start + r, ""))
|
|
220
|
+
|
|
221
|
+
def render_detail(out, W, start, body_h):
|
|
222
|
+
if not ui.filtered:
|
|
223
|
+
for r in range(body_h):
|
|
224
|
+
out.append(wline(start + r, ""))
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
j = ui.filtered[ui.sel]
|
|
228
|
+
lines = []
|
|
229
|
+
lines.append(f"\033[1mName:\033[0m /{j['name']}")
|
|
230
|
+
lines.append(f"\033[1mFolder:\033[0m {j['folder']}/")
|
|
231
|
+
lines.append(f"\033[1mSource:\033[0m {j['source']}")
|
|
232
|
+
lines.append(f"\033[1mInteractive:\033[0m {'yes' if j['interactive'] else 'no'}")
|
|
233
|
+
lines.append("")
|
|
234
|
+
lines.append(f"\033[1mDescription:\033[0m")
|
|
235
|
+
# Wrap description
|
|
236
|
+
desc = j['description']
|
|
237
|
+
while desc:
|
|
238
|
+
lines.append(f" {desc[:W-4]}")
|
|
239
|
+
desc = desc[W-4:]
|
|
240
|
+
lines.append("")
|
|
241
|
+
lines.append(f"\033[1mInputs:\033[0m")
|
|
242
|
+
if j['inputs']:
|
|
243
|
+
for inp in j['inputs']:
|
|
244
|
+
if isinstance(inp, dict):
|
|
245
|
+
for k, v in inp.items():
|
|
246
|
+
default = f" = {v}" if v is not None and v != '' else ""
|
|
247
|
+
lines.append(f" - {k}{default}")
|
|
248
|
+
elif isinstance(inp, str):
|
|
249
|
+
lines.append(f" - {inp}")
|
|
250
|
+
else:
|
|
251
|
+
lines.append(f" - {inp}")
|
|
252
|
+
else:
|
|
253
|
+
lines.append(" (none)")
|
|
254
|
+
lines.append("")
|
|
255
|
+
lines.append(f"\033[1mPath:\033[0m")
|
|
256
|
+
lines.append(f" {j['path']}")
|
|
257
|
+
|
|
258
|
+
vis = lines[ui.detail_scroll:ui.detail_scroll + body_h]
|
|
259
|
+
for r in range(body_h):
|
|
260
|
+
row = start + r
|
|
261
|
+
if r < len(vis):
|
|
262
|
+
out.append(wline(row, f" {vis[r]}"))
|
|
263
|
+
else:
|
|
264
|
+
out.append(wline(row, ""))
|
|
265
|
+
|
|
266
|
+
# ── input handling ─────────────────────────────────────
|
|
267
|
+
def handle(c):
|
|
268
|
+
if ui.search_mode:
|
|
269
|
+
return handle_search(c)
|
|
270
|
+
if c == '\x1b':
|
|
271
|
+
return handle_esc()
|
|
272
|
+
if c == 'q':
|
|
273
|
+
if ui.detail:
|
|
274
|
+
ui.detail = False
|
|
275
|
+
ui.detail_scroll = 0
|
|
276
|
+
ui.status = ""
|
|
277
|
+
else:
|
|
278
|
+
return False
|
|
279
|
+
elif c == '\t':
|
|
280
|
+
ui.tab = (ui.tab + 1) % len(ui.tabs)
|
|
281
|
+
ui.sel = 0
|
|
282
|
+
ui.scroll = 0
|
|
283
|
+
ui.detail = False
|
|
284
|
+
ui.status = ""
|
|
285
|
+
apply_filters()
|
|
286
|
+
elif c == 'k':
|
|
287
|
+
nav_up()
|
|
288
|
+
elif c == 'j':
|
|
289
|
+
nav_down()
|
|
290
|
+
elif c in ('\r', '\n'):
|
|
291
|
+
do_enter()
|
|
292
|
+
elif c == '/':
|
|
293
|
+
ui.search_mode = True
|
|
294
|
+
ui.search_buf = ui.search_query
|
|
295
|
+
ui.status = ""
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
def handle_esc():
|
|
299
|
+
if select.select([sys.stdin], [], [], 0.05)[0]:
|
|
300
|
+
c2 = sys.stdin.read(1)
|
|
301
|
+
if c2 == '[':
|
|
302
|
+
c3 = sys.stdin.read(1)
|
|
303
|
+
if c3 == 'A':
|
|
304
|
+
nav_up()
|
|
305
|
+
elif c3 == 'B':
|
|
306
|
+
nav_down()
|
|
307
|
+
# consume any other escape sequence
|
|
308
|
+
else:
|
|
309
|
+
# bare Esc
|
|
310
|
+
if ui.detail:
|
|
311
|
+
ui.detail = False
|
|
312
|
+
ui.detail_scroll = 0
|
|
313
|
+
ui.status = ""
|
|
314
|
+
elif ui.search_query:
|
|
315
|
+
ui.search_query = ""
|
|
316
|
+
ui.sel = 0
|
|
317
|
+
ui.scroll = 0
|
|
318
|
+
apply_filters()
|
|
319
|
+
ui.status = "Search cleared"
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
def handle_search(c):
|
|
323
|
+
if c == '\x1b':
|
|
324
|
+
# Check if arrow key or bare esc
|
|
325
|
+
if select.select([sys.stdin], [], [], 0.05)[0]:
|
|
326
|
+
c2 = sys.stdin.read(1)
|
|
327
|
+
if c2 == '[':
|
|
328
|
+
sys.stdin.read(1) # consume arrow char
|
|
329
|
+
else:
|
|
330
|
+
ui.search_mode = False
|
|
331
|
+
ui.search_buf = ""
|
|
332
|
+
ui.status = "Search cancelled"
|
|
333
|
+
elif c in ('\r', '\n'):
|
|
334
|
+
ui.search_mode = False
|
|
335
|
+
ui.search_query = ui.search_buf
|
|
336
|
+
ui.search_buf = ""
|
|
337
|
+
ui.sel = 0
|
|
338
|
+
ui.scroll = 0
|
|
339
|
+
apply_filters()
|
|
340
|
+
if ui.search_query:
|
|
341
|
+
ui.status = f"Filter: \"{ui.search_query}\" ({len(ui.filtered)} results)"
|
|
342
|
+
else:
|
|
343
|
+
ui.status = "Search cleared"
|
|
344
|
+
elif c in ('\x7f', '\x08'):
|
|
345
|
+
ui.search_buf = ui.search_buf[:-1]
|
|
346
|
+
elif c == '\x15': # Ctrl+U clear line
|
|
347
|
+
ui.search_buf = ""
|
|
348
|
+
elif 32 <= ord(c) <= 126:
|
|
349
|
+
ui.search_buf += c
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
def nav_up():
|
|
353
|
+
if ui.detail:
|
|
354
|
+
ui.detail_scroll = max(0, ui.detail_scroll - 1)
|
|
355
|
+
else:
|
|
356
|
+
ui.sel = max(0, ui.sel - 1)
|
|
357
|
+
if ui.sel < ui.scroll:
|
|
358
|
+
ui.scroll = ui.sel
|
|
359
|
+
ui.status = ""
|
|
360
|
+
|
|
361
|
+
def nav_down():
|
|
362
|
+
_, H = term_size()
|
|
363
|
+
body_h = H - 8 # body_start=6, footer uses 3 lines
|
|
364
|
+
if body_h < 1:
|
|
365
|
+
body_h = 1
|
|
366
|
+
if ui.detail:
|
|
367
|
+
ui.detail_scroll += 1
|
|
368
|
+
else:
|
|
369
|
+
mx = max(0, len(ui.filtered) - 1)
|
|
370
|
+
ui.sel = min(mx, ui.sel + 1)
|
|
371
|
+
if ui.sel >= ui.scroll + body_h:
|
|
372
|
+
ui.scroll = ui.sel - body_h + 1
|
|
373
|
+
ui.status = ""
|
|
374
|
+
|
|
375
|
+
def do_enter():
|
|
376
|
+
if ui.detail:
|
|
377
|
+
# Already in detail, do nothing extra
|
|
378
|
+
pass
|
|
379
|
+
elif ui.filtered:
|
|
380
|
+
ui.detail = True
|
|
381
|
+
ui.detail_scroll = 0
|
|
382
|
+
ui.status = ""
|
|
383
|
+
|
|
384
|
+
# ── main loop ──────────────────────────────────────────
|
|
385
|
+
ui.all_jinxs = load_jinxs()
|
|
386
|
+
apply_filters()
|
|
387
|
+
|
|
388
|
+
fd = sys.stdin.fileno()
|
|
389
|
+
old_attrs = termios.tcgetattr(fd)
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
tty.setcbreak(fd)
|
|
393
|
+
sys.stdout.write('\033[?25l') # hide cursor
|
|
394
|
+
sys.stdout.write('\033[2J\033[H') # initial full clear
|
|
395
|
+
sys.stdout.flush()
|
|
396
|
+
render()
|
|
397
|
+
while True:
|
|
398
|
+
c = sys.stdin.read(1)
|
|
399
|
+
if not handle(c):
|
|
400
|
+
break
|
|
401
|
+
render()
|
|
402
|
+
finally:
|
|
403
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
|
|
404
|
+
sys.stdout.write('\033[?25h\033[2J\033[H')
|
|
405
|
+
sys.stdout.flush()
|
|
406
|
+
|
|
407
|
+
context['output'] = "Jinxs browser closed."
|