npcsh 1.1.19__py3-none-any.whl → 1.1.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. npcsh/_state.py +11 -7
  2. npcsh/npc_team/jinxs/bin/config_tui.jinx +3 -2
  3. npcsh/npc_team/jinxs/bin/jinxs.jinx +407 -0
  4. npcsh/npc_team/jinxs/bin/kg.jinx +941 -0
  5. npcsh/npc_team/jinxs/bin/memories.jinx +3 -2
  6. npcsh/npc_team/jinxs/bin/models.jinx +343 -0
  7. npcsh/npc_team/jinxs/bin/nql.jinx +380 -50
  8. npcsh/npc_team/jinxs/bin/setup.jinx +2 -1
  9. npcsh/npc_team/jinxs/bin/team.jinx +504 -0
  10. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +1 -1
  12. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +1 -1
  13. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +1 -1
  14. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +1 -1
  15. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +1 -1
  16. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +1 -1
  17. npcsh/npc_team/jinxs/modes/alicanto.jinx +1 -1
  18. npcsh/npc_team/jinxs/modes/arxiv.jinx +1 -1
  19. npcsh/npc_team/jinxs/modes/corca.jinx +1 -1
  20. npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
  21. npcsh/npc_team/jinxs/modes/plonk.jinx +1 -1
  22. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  23. npcsh/npc_team/jinxs/modes/reattach.jinx +1 -1
  24. npcsh/npc_team/jinxs/modes/spool.jinx +1 -1
  25. npcsh/npc_team/jinxs/modes/wander.jinx +1 -1
  26. npcsh/routes.py +8 -2
  27. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.jinx +1 -1
  28. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/arxiv.jinx +1 -1
  29. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/config_tui.jinx +3 -2
  30. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.jinx +1 -1
  31. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/db_search.jinx +1 -1
  32. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/file_search.jinx +1 -1
  33. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.jinx +4 -4
  34. npcsh-1.1.20.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  35. npcsh-1.1.20.data/data/npcsh/npc_team/kg.jinx +941 -0
  36. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kg_search.jinx +1 -1
  37. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_search.jinx +1 -1
  38. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/memories.jinx +3 -2
  39. npcsh-1.1.20.data/data/npcsh/npc_team/models.jinx +343 -0
  40. npcsh-1.1.20.data/data/npcsh/npc_team/nql.jinx +471 -0
  41. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paper_search.jinx +1 -1
  42. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.jinx +1 -1
  43. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/pti.jinx +1 -1
  44. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/reattach.jinx +1 -1
  45. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/semantic_scholar.jinx +1 -1
  46. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/setup.jinx +2 -1
  47. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.jinx +1 -1
  48. npcsh-1.1.20.data/data/npcsh/npc_team/team.jinx +504 -0
  49. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wander.jinx +1 -1
  50. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/web_search.jinx +1 -1
  51. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/METADATA +1 -1
  52. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/RECORD +139 -135
  53. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/entry_points.txt +4 -1
  54. npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
  55. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -331
  56. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  57. npcsh-1.1.19.data/data/npcsh/npc_team/nql.jinx +0 -141
  58. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +0 -327
  59. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  60. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  61. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.png +0 -0
  62. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  63. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  64. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  65. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/build.jinx +0 -0
  66. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/chat.jinx +0 -0
  67. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/click.jinx +0 -0
  68. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  69. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  70. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  71. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  72. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compile.jinx +0 -0
  73. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compress.jinx +0 -0
  74. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  75. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/convene.jinx +0 -0
  76. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.npc +0 -0
  77. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.png +0 -0
  78. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca_example.png +0 -0
  79. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  80. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  81. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  82. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic.npc +0 -0
  83. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic4.png +0 -0
  84. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.npc +0 -0
  85. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.png +0 -0
  86. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/help.jinx +0 -0
  87. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  88. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/init.jinx +0 -0
  89. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  90. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  91. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  92. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  93. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  94. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  95. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
  96. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  97. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/notify.jinx +0 -0
  98. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  99. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  100. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  101. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  102. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/ots.jinx +0 -0
  103. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paste.jinx +0 -0
  104. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.npc +0 -0
  105. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.png +0 -0
  106. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  107. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  108. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/python.jinx +0 -0
  109. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  110. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/roll.jinx +0 -0
  111. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  112. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sample.jinx +0 -0
  113. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  114. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/search.jinx +0 -0
  115. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  116. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/serve.jinx +0 -0
  117. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/set.jinx +0 -0
  118. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sh.jinx +0 -0
  119. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/shh.jinx +0 -0
  120. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  121. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.png +0 -0
  122. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  123. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  124. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.png +0 -0
  125. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sql.jinx +0 -0
  126. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch.jinx +0 -0
  127. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  128. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  129. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switches.jinx +0 -0
  130. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sync.jinx +0 -0
  131. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  132. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  133. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  134. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/usage.jinx +0 -0
  135. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  136. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  137. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wait.jinx +0 -0
  138. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  139. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.jinx +0 -0
  140. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.png +0 -0
  141. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  142. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/WHEEL +0 -0
  143. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/licenses/LICENSE +0 -0
  144. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/top_level.txt +0 -0
@@ -118,7 +118,7 @@ steps:
118
118
  color = ''
119
119
 
120
120
  if idx == selected:
121
- sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
121
+ sys.stdout.write(f'\033[7;1m>{line.ljust(width-2)}\033[0m')
122
122
  elif color:
123
123
  sys.stdout.write(f'{color}{line}\033[0m')
124
124
  else:
@@ -217,7 +217,7 @@ steps:
217
217
  panel_tabs = ""
218
218
  for i, name in enumerate(["Output", "Streams", "Starred"]):
219
219
  if i == state.current_panel:
220
- panel_tabs += f"\033[47;30m {name} \033[0m "
220
+ panel_tabs += f"\033[7m {name} \033[0m "
221
221
  else:
222
222
  panel_tabs += f"\033[90m {name} \033[0m "
223
223
 
npcsh/routes.py CHANGED
@@ -12,6 +12,7 @@ class CommandRouter:
12
12
  self.routes = {}
13
13
  self.help_info = {}
14
14
  self.jinx_routes = {}
15
+ self.jinx_objects = {} # command_name -> Jinx object
15
16
 
16
17
  def route(self, command: str, help_text: str = "") -> Callable:
17
18
  def wrapper(func):
@@ -42,12 +43,17 @@ class CommandRouter:
42
43
 
43
44
  def register_jinx(self, jinx: Jinx):
44
45
  command_name = jinx.jinx_name
45
-
46
+
46
47
  def jinx_handler(command: str, **kwargs):
47
48
  return self._execute_jinx(jinx, command, **kwargs)
48
-
49
+
49
50
  self.jinx_routes[command_name] = jinx_handler
51
+ self.jinx_objects[command_name] = jinx
50
52
  self.help_info[command_name] = jinx.description or "Jinx command"
53
+
54
+ def is_interactive(self, command_name: str) -> bool:
55
+ jinx = self.jinx_objects.get(command_name)
56
+ return bool(jinx and getattr(jinx, 'interactive', False))
51
57
 
52
58
  def _execute_jinx(self, jinx: Jinx, command: str, **kwargs):
53
59
  messages = kwargs.get("messages", [])
@@ -114,7 +114,7 @@ steps:
114
114
 
115
115
  line = str(items[idx])[:width-2]
116
116
  if current_tab in [0, 1, 2] and idx == selected:
117
- sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
117
+ sys.stdout.write(f'\033[7;1m>{line.ljust(width-2)}\033[0m')
118
118
  else:
119
119
  # Color gold/cliff markers
120
120
  if '[GOLD]' in line:
@@ -451,7 +451,7 @@ steps:
451
451
  line = line[:width-1]
452
452
 
453
453
  if idx == selected:
454
- sys.stdout.write('\033[47;30;1m>' + line.ljust(width-1) + '\033[0m')
454
+ sys.stdout.write('\033[7;1m>' + line.ljust(width-1) + '\033[0m')
455
455
  else:
456
456
  sys.stdout.write(' ' + line)
457
457
 
@@ -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
 
@@ -129,7 +129,7 @@ steps:
129
129
  line = line[:width-1]
130
130
 
131
131
  if idx == selected:
132
- sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
132
+ sys.stdout.write(f'\033[7;1m>{line}\033[0m')
133
133
  else:
134
134
  sys.stdout.write(f' {line}')
135
135
 
@@ -213,7 +213,7 @@ steps:
213
213
  line = line[:width+20] # allow for color codes
214
214
 
215
215
  if idx == selected:
216
- sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
216
+ sys.stdout.write(f'\033[7;1m>{line}\033[0m')
217
217
  else:
218
218
  sys.stdout.write(f' {line}')
219
219
 
@@ -207,7 +207,7 @@ steps:
207
207
  line = line[:width-1]
208
208
 
209
209
  if idx == selected:
210
- sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
210
+ sys.stdout.write(f'\033[7;1m>{line}\033[0m')
211
211
  else:
212
212
  sys.stdout.write(f' {line}')
213
213
 
@@ -1,5 +1,5 @@
1
1
  jinx_name: guac
2
- description: Interactive Python data analysis TUI - live variable inspector, DataFrame viewer, code execution
2
+ description: Interactive Python TUI - live variable inspector, code execution, DataFrame viewer
3
3
  inputs:
4
4
  - model: null
5
5
  - provider: null
@@ -202,7 +202,7 @@ steps:
202
202
  # ===== HEADER =====
203
203
  mode_colors = {"code": "\033[32m", "natural": "\033[35m", "inspect": "\033[33m"}
204
204
  mode_str = f"{mode_colors[state.mode]}[{state.mode}]\033[0m"
205
- header = f" GUAC - Python Data Analysis {mode_str} "
205
+ header = f" GUAC - Interactive Python {mode_str} "
206
206
  status_color = "\033[33m" if "..." in state.status else "\033[32m"
207
207
  out.append(f"\033[1;1H\033[42;30;1m{header.ljust(width)}\033[0m")
208
208
  out.append(f"\033[1;{width-len(state.status)-3}H{status_color}[{state.status}]\033[0m")
@@ -275,7 +275,7 @@ steps:
275
275
  tabs = ""
276
276
  for i, name in enumerate(panel_names):
277
277
  if i == state.panel:
278
- tabs += f"\033[47;30m {name} \033[0m"
278
+ tabs += f"\033[7m {name} \033[0m"
279
279
  else:
280
280
  tabs += f"\033[90m {name} \033[0m"
281
281
  out.append(f"\033[3;{right_x}H{tabs}")
@@ -291,7 +291,7 @@ steps:
291
291
  info = var_info(name, value)
292
292
  display = f"{name[:10]:<10} {info[:rpanel_w-12]}"
293
293
  if idx == state.selected_var:
294
- out.append(f"\033[{4+i};{right_x}H\033[47;30m>{display[:rpanel_w]}\033[0m")
294
+ out.append(f"\033[{4+i};{right_x}H\033[7m>{display[:rpanel_w]}\033[0m")
295
295
  elif isinstance(value, pd.DataFrame):
296
296
  out.append(f"\033[{4+i};{right_x}H\033[34m {display[:rpanel_w]}\033[0m")
297
297
  elif isinstance(value, np.ndarray):
@@ -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."