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.
Files changed (153) hide show
  1. {npcsh-1.1.19/npcsh.egg-info → npcsh-1.1.20}/PKG-INFO +1 -1
  2. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/_state.py +11 -7
  3. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/config_tui.jinx +3 -2
  4. npcsh-1.1.20/npcsh/npc_team/jinxs/bin/jinxs.jinx +407 -0
  5. npcsh-1.1.20/npcsh/npc_team/jinxs/bin/kg.jinx +941 -0
  6. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/memories.jinx +3 -2
  7. npcsh-1.1.20/npcsh/npc_team/jinxs/bin/models.jinx +343 -0
  8. npcsh-1.1.20/npcsh/npc_team/jinxs/bin/nql.jinx +471 -0
  9. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/setup.jinx +2 -1
  10. npcsh-1.1.20/npcsh/npc_team/jinxs/bin/team.jinx +504 -0
  11. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +1 -1
  12. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +1 -1
  13. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +1 -1
  14. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +1 -1
  15. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +1 -1
  16. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/research/paper_search.jinx +1 -1
  17. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +1 -1
  18. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/alicanto.jinx +1 -1
  19. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/arxiv.jinx +1 -1
  20. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/corca.jinx +1 -1
  21. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
  22. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/plonk.jinx +1 -1
  23. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  24. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/reattach.jinx +1 -1
  25. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/spool.jinx +1 -1
  26. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/wander.jinx +1 -1
  27. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/routes.py +8 -2
  28. {npcsh-1.1.19 → npcsh-1.1.20/npcsh.egg-info}/PKG-INFO +1 -1
  29. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/SOURCES.txt +4 -2
  30. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/entry_points.txt +4 -1
  31. {npcsh-1.1.19 → npcsh-1.1.20}/setup.py +1 -1
  32. npcsh-1.1.19/npcsh/npc_team/jinxs/bin/nql.jinx +0 -141
  33. npcsh-1.1.19/npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
  34. npcsh-1.1.19/npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -331
  35. {npcsh-1.1.19 → npcsh-1.1.20}/LICENSE +0 -0
  36. {npcsh-1.1.19 → npcsh-1.1.20}/MANIFEST.in +0 -0
  37. {npcsh-1.1.19 → npcsh-1.1.20}/README.md +0 -0
  38. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/__init__.py +0 -0
  39. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/alicanto.py +0 -0
  40. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/benchmark/__init__.py +0 -0
  41. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/benchmark/npcsh_agent.py +0 -0
  42. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/benchmark/runner.py +0 -0
  43. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/benchmark/templates/install-npcsh.sh.j2 +0 -0
  44. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/build.py +0 -0
  45. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/completion.py +0 -0
  46. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/config.py +0 -0
  47. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/conversation_viewer.py +0 -0
  48. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/corca.py +0 -0
  49. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/diff_viewer.py +0 -0
  50. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/execution.py +0 -0
  51. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/guac.py +0 -0
  52. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/mcp_helpers.py +0 -0
  53. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/mcp_server.py +0 -0
  54. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc.py +0 -0
  55. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/alicanto.npc +0 -0
  56. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/alicanto.png +0 -0
  57. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/corca.npc +0 -0
  58. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/corca.png +0 -0
  59. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/corca_example.png +0 -0
  60. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/frederic.npc +0 -0
  61. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/frederic4.png +0 -0
  62. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/guac.npc +0 -0
  63. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/guac.png +0 -0
  64. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/benchmark.jinx +0 -0
  65. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/roll.jinx +0 -0
  66. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/sample.jinx +0 -0
  67. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/sync.jinx +0 -0
  68. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -0
  69. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -0
  70. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -0
  71. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -0
  72. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -0
  73. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -0
  74. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/incognide.jinx +0 -0
  75. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -0
  76. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -0
  77. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/notify.jinx +0 -0
  78. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -0
  79. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -0
  80. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -0
  81. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -0
  82. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -0
  83. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -0
  84. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -0
  85. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -0
  86. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -0
  87. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +0 -0
  88. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +0 -0
  89. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/browser/close_browser.jinx +0 -0
  90. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +0 -0
  91. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/click.jinx +0 -0
  92. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +0 -0
  93. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +0 -0
  94. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +0 -0
  95. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +0 -0
  96. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +0 -0
  97. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +0 -0
  98. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/chat.jinx +0 -0
  99. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/cmd.jinx +0 -0
  100. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/compress.jinx +0 -0
  101. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/edit_file.jinx +0 -0
  102. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/load_file.jinx +0 -0
  103. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/ots.jinx +0 -0
  104. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/paste.jinx +0 -0
  105. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/python.jinx +0 -0
  106. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -0
  107. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/search.jinx +0 -0
  108. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/sh.jinx +0 -0
  109. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/sleep.jinx +0 -0
  110. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/core/sql.jinx +0 -0
  111. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +0 -0
  112. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +0 -0
  113. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -0
  114. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/compile.jinx +0 -0
  115. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/help.jinx +0 -0
  116. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/init.jinx +0 -0
  117. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/serve.jinx +0 -0
  118. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/set.jinx +0 -0
  119. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/shh.jinx +0 -0
  120. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/switch.jinx +0 -0
  121. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/switches.jinx +0 -0
  122. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +0 -0
  123. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/usage.jinx +0 -0
  124. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/lib/utils/verbose.jinx +0 -0
  125. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/jinxs/modes/yap.jinx +0 -0
  126. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/kadiefa.npc +0 -0
  127. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/kadiefa.png +0 -0
  128. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/npcsh.ctx +0 -0
  129. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/npcsh_sibiji.png +0 -0
  130. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/plonk.npc +0 -0
  131. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/plonk.png +0 -0
  132. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/plonkjr.npc +0 -0
  133. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/plonkjr.png +0 -0
  134. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/sibiji.npc +0 -0
  135. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/sibiji.png +0 -0
  136. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/spool.png +0 -0
  137. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npc_team/yap.png +0 -0
  138. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/npcsh.py +0 -0
  139. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/parsing.py +0 -0
  140. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/plonk.py +0 -0
  141. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/pti.py +0 -0
  142. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/spool.py +0 -0
  143. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/ui.py +0 -0
  144. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/wander.py +0 -0
  145. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh/yap.py +0 -0
  146. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/dependency_links.txt +0 -0
  147. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/requires.txt +0 -0
  148. {npcsh-1.1.19 → npcsh-1.1.20}/npcsh.egg-info/top_level.txt +0 -0
  149. {npcsh-1.1.19 → npcsh-1.1.20}/project/__init__.py +0 -0
  150. {npcsh-1.1.19 → npcsh-1.1.20}/setup.cfg +0 -0
  151. {npcsh-1.1.19 → npcsh-1.1.20}/tests/test_config.py +0 -0
  152. {npcsh-1.1.19 → npcsh-1.1.20}/tests/test_jinxs.py +0 -0
  153. {npcsh-1.1.19 → npcsh-1.1.20}/tests/test_tool_routing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.1.19
3
+ Version: 1.1.20
4
4
  Summary: npcsh is a command-line toolkit for using AI agents in novel ways.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcsh
6
6
  Author: Christopher Agostino
@@ -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 by looking for the jinx file in modes/
2763
+ # Check if this is an interactive mode
2764
2764
  is_interactive_mode = False
2765
-
2766
- # Check global modes
2767
- global_modes_jinx = os.path.expanduser(f'~/.npcsh/npc_team/jinxs/modes/{command_name}.jinx')
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
- # Check team modes
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[47;30m{item['label']:<{label_width}}\033[0m")
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[47;30m{mod_indicator}{item['label']:<{label_width}} {display_value[:value_width]}\033[0m")
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."