npcsh 1.1.17__py3-none-any.whl → 1.1.18__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 (169) hide show
  1. npcsh/_state.py +114 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +46 -12
  5. npcsh/benchmark/runner.py +85 -43
  6. npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
  7. npcsh/build.py +2 -4
  8. npcsh/completion.py +2 -6
  9. npcsh/config.py +1 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/execution.py +0 -1
  13. npcsh/guac.py +0 -1
  14. npcsh/mcp_helpers.py +2 -3
  15. npcsh/mcp_server.py +5 -10
  16. npcsh/npc.py +10 -11
  17. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  18. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  19. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  20. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  21. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  22. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  23. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  24. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  25. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  26. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  27. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  28. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  29. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  30. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  31. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  32. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  33. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  34. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  35. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  36. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  37. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  38. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  39. npcsh/npcsh.py +7 -4
  40. npcsh/plonk.py +0 -1
  41. npcsh/pti.py +0 -1
  42. npcsh/routes.py +1 -3
  43. npcsh/spool.py +0 -1
  44. npcsh/ui.py +0 -1
  45. npcsh/wander.py +0 -1
  46. npcsh/yap.py +0 -1
  47. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  48. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  49. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  50. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  51. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  52. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  53. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  54. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  55. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  56. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  57. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  58. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  59. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  60. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  61. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  62. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  63. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  64. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  65. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  66. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  67. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  68. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.jinx +13 -7
  69. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/METADATA +90 -1
  70. npcsh-1.1.18.dist-info/RECORD +235 -0
  71. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  72. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +0 -3
  73. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  74. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  75. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  76. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  77. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  78. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  79. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  80. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  81. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  82. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  83. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  84. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  85. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  86. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  87. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  88. npcsh-1.1.17.dist-info/RECORD +0 -219
  89. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  90. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  91. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  92. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  93. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  94. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +0 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +0 -0
  96. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +0 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  98. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  100. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +0 -0
  102. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +0 -0
  103. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  104. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +0 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  106. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  107. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  108. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  109. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  110. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  112. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  113. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  114. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  115. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +0 -0
  116. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  117. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +0 -0
  118. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  119. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  120. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  121. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  122. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  123. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  124. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  125. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/notify.jinx +0 -0
  126. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  127. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  128. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +0 -0
  129. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  130. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  131. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +0 -0
  132. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  133. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  134. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  135. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/search.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  168. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  169. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,331 @@
1
+ jinx_name: jinxs
2
+ description: Interactive browser for available jinxs
3
+ inputs:
4
+ - path: ""
5
+ - text: "false"
6
+
7
+ steps:
8
+ - name: list_jinxs
9
+ engine: python
10
+ code: |
11
+ import os
12
+ import sys
13
+ import tty
14
+ import termios
15
+ from pathlib import Path
16
+ import yaml
17
+
18
+ filter_path = context.get('path', '').strip()
19
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
20
+
21
+ # Find jinxs directory from team or fallback
22
+ jinxs_dir = None
23
+ if hasattr(npc, 'team') and npc.team:
24
+ if hasattr(npc.team, 'jinxs_dir') and npc.team.jinxs_dir:
25
+ jinxs_dir = Path(npc.team.jinxs_dir)
26
+ elif hasattr(npc.team, 'team_path') and npc.team.team_path:
27
+ candidate = Path(npc.team.team_path) / "jinxs"
28
+ if candidate.exists():
29
+ jinxs_dir = candidate
30
+
31
+ if not jinxs_dir:
32
+ global_jinxs = Path.home() / ".npcsh" / "npc_team" / "jinxs"
33
+ if global_jinxs.exists():
34
+ jinxs_dir = global_jinxs
35
+
36
+ if not jinxs_dir or not jinxs_dir.exists():
37
+ context['output'] = "Error: Could not find jinxs directory"
38
+ else:
39
+ def get_jinx_info(jinx_path):
40
+ try:
41
+ with open(jinx_path, 'r') as f:
42
+ content = f.read()
43
+ header = content.split('steps:')[0] if 'steps:' in content else content
44
+ data = yaml.safe_load(header)
45
+ name = data.get('jinx_name', jinx_path.stem)
46
+ desc = data.get('description', 'No description')
47
+ inputs = data.get('inputs', [])
48
+ return name, desc, inputs
49
+ except:
50
+ return jinx_path.stem, 'No description', []
51
+
52
+ def get_all_jinxs(base_path):
53
+ jinxs = []
54
+ folders = set()
55
+ for root, dirs, files in os.walk(base_path):
56
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
57
+ rel_path = Path(root).relative_to(base_path)
58
+ folder = str(rel_path) if str(rel_path) != '.' else 'root'
59
+ if folder != 'root':
60
+ folders.add(folder.split('/')[0])
61
+ for jf in files:
62
+ if jf.endswith('.jinx'):
63
+ jinx_path = Path(root) / jf
64
+ name, desc, inputs = get_jinx_info(jinx_path)
65
+ jinxs.append({
66
+ 'name': name,
67
+ 'description': desc,
68
+ 'inputs': inputs,
69
+ 'folder': folder,
70
+ 'path': str(jinx_path)
71
+ })
72
+ return jinxs, sorted(folders)
73
+
74
+ all_jinxs, folders = get_all_jinxs(jinxs_dir)
75
+ folders = ['root'] + list(folders)
76
+
77
+ if text_mode or not all_jinxs:
78
+ # Text-only output
79
+ output_lines = ["Available Jinxs", "=" * 40, ""]
80
+ by_folder = {}
81
+ for j in all_jinxs:
82
+ f = j['folder']
83
+ if f not in by_folder:
84
+ by_folder[f] = []
85
+ by_folder[f].append(j)
86
+
87
+ for folder in sorted(by_folder.keys()):
88
+ if folder == 'root':
89
+ output_lines.append("Root:")
90
+ else:
91
+ output_lines.append(f"{folder}/:")
92
+ for j in by_folder[folder]:
93
+ output_lines.append(f" /{j['name']} - {j['description'][:50]}")
94
+ output_lines.append("")
95
+
96
+ output_lines.append("Use /jinxs path=<folder> for details")
97
+ output_lines.append("Use text=false for interactive TUI")
98
+ context['output'] = "\n".join(output_lines)
99
+ else:
100
+ # Interactive TUI mode
101
+ def get_terminal_size():
102
+ try:
103
+ size = os.get_terminal_size()
104
+ return size.columns, size.lines
105
+ except:
106
+ return 80, 24
107
+
108
+ width, height = get_terminal_size()
109
+ selected_folder = 0
110
+ selected_jinx = 0
111
+ jinx_scroll = 0
112
+ list_height = height - 5
113
+ mode = 'list' # list or preview
114
+ preview_scroll = 0
115
+ filter_text = ''
116
+
117
+ def get_jinxs_in_folder(folder):
118
+ if folder == 'root':
119
+ return [j for j in all_jinxs if j['folder'] == 'root']
120
+ return [j for j in all_jinxs if j['folder'].startswith(folder)]
121
+
122
+ def filter_jinxs(jinxs, text):
123
+ if not text:
124
+ return jinxs
125
+ text = text.lower()
126
+ return [j for j in jinxs if text in j['name'].lower() or text in j['description'].lower()]
127
+
128
+ current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
129
+
130
+ fd = sys.stdin.fileno()
131
+ old_settings = termios.tcgetattr(fd)
132
+
133
+ try:
134
+ tty.setcbreak(fd)
135
+ sys.stdout.write('\033[?25l')
136
+ sys.stdout.write('\033[2J\033[H')
137
+
138
+ while True:
139
+ width, height = get_terminal_size()
140
+ list_height = height - 5
141
+
142
+ if mode == 'list':
143
+ if selected_jinx < jinx_scroll:
144
+ jinx_scroll = selected_jinx
145
+ elif selected_jinx >= jinx_scroll + list_height:
146
+ jinx_scroll = selected_jinx - list_height + 1
147
+
148
+ sys.stdout.write('\033[H')
149
+
150
+ # Header
151
+ if mode == 'list':
152
+ f = folders[selected_folder] if folders else 'none'
153
+ flt = f" filter:'{filter_text}'" if filter_text else ""
154
+ header = f" JINXS ({len(current_jinxs)}): {f}{flt} "
155
+ else:
156
+ j = current_jinxs[selected_jinx] if current_jinxs else {}
157
+ header = f" PREVIEW: /{j.get('name', '')} "
158
+ sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
159
+
160
+ # Folder bar
161
+ folder_bar = ""
162
+ for i, f in enumerate(folders[:8]):
163
+ if i == selected_folder:
164
+ folder_bar += f'\033[47;30m [{f[:8]}] \033[0m'
165
+ else:
166
+ folder_bar += f' {f[:8]} '
167
+ if len(folders) > 8:
168
+ folder_bar += f'...+{len(folders)-8}'
169
+ sys.stdout.write(f'{folder_bar[:width]}\n')
170
+
171
+ if mode == 'list':
172
+ for i in range(list_height):
173
+ idx = jinx_scroll + i
174
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
175
+ if idx >= len(current_jinxs):
176
+ continue
177
+
178
+ j = current_jinxs[idx]
179
+ name = j['name'][:20]
180
+ desc = j['description'][:width-25]
181
+
182
+ if idx == selected_jinx:
183
+ sys.stdout.write(f'\033[47;30;1m>/{name:<20} {desc}\033[0m')
184
+ else:
185
+ sys.stdout.write(f' /{name:<20} {desc}')
186
+
187
+ # Status bar
188
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
189
+ j = current_jinxs[selected_jinx] if current_jinxs else {}
190
+ path = j.get('path', '')[-50:] if j else ''
191
+ sys.stdout.write(f'\033[{height-1};1H\033[K {path}'.ljust(width))
192
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m h/l:Folder j/k:Jinx p:Preview Enter:Run f:Filter q:Quit [{selected_jinx+1}/{len(current_jinxs)}] \033[0m')
193
+
194
+ else: # preview mode
195
+ j = current_jinxs[selected_jinx]
196
+ preview_lines = [
197
+ f"Name: /{j['name']}",
198
+ "",
199
+ f"Description:",
200
+ f" {j['description']}",
201
+ "",
202
+ f"Path: {j['path']}",
203
+ f"Folder: {j['folder']}",
204
+ "",
205
+ ]
206
+
207
+ if j.get('inputs'):
208
+ preview_lines.append("Inputs:")
209
+ for inp in j['inputs']:
210
+ if isinstance(inp, dict):
211
+ for k, v in inp.items():
212
+ default = f" (default: {v})" if v else ""
213
+ preview_lines.append(f" - {k}{default}")
214
+ else:
215
+ preview_lines.append(f" - {inp}")
216
+ preview_lines.append("")
217
+
218
+ preview_lines.append("Usage:")
219
+ usage = f" /{j['name']}"
220
+ if j.get('inputs'):
221
+ for inp in j['inputs'][:3]:
222
+ if isinstance(inp, dict):
223
+ for k, v in inp.items():
224
+ usage += f" {k}=<value>"
225
+ preview_lines.append(usage)
226
+
227
+ for i in range(list_height):
228
+ idx = preview_scroll + i
229
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
230
+ if idx < len(preview_lines):
231
+ sys.stdout.write(preview_lines[idx][:width-1])
232
+
233
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
234
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(preview_lines)} lines]')
235
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back Enter:Run q:Quit \033[0m')
236
+
237
+ sys.stdout.flush()
238
+
239
+ c = sys.stdin.read(1)
240
+
241
+ if c == '\x1b':
242
+ c2 = sys.stdin.read(1)
243
+ if c2 == '[':
244
+ c3 = sys.stdin.read(1)
245
+ if c3 == 'A': # Up
246
+ if mode == 'list' and selected_jinx > 0:
247
+ selected_jinx -= 1
248
+ elif mode == 'preview' and preview_scroll > 0:
249
+ preview_scroll -= 1
250
+ elif c3 == 'B': # Down
251
+ if mode == 'list' and selected_jinx < len(current_jinxs) - 1:
252
+ selected_jinx += 1
253
+ elif mode == 'preview' and preview_scroll < 50:
254
+ preview_scroll += 1
255
+ elif c3 == 'C': # Right
256
+ if mode == 'list' and selected_folder < len(folders) - 1:
257
+ selected_folder += 1
258
+ current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
259
+ selected_jinx = 0
260
+ jinx_scroll = 0
261
+ elif c3 == 'D': # Left
262
+ if mode == 'list' and selected_folder > 0:
263
+ selected_folder -= 1
264
+ current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
265
+ selected_jinx = 0
266
+ jinx_scroll = 0
267
+ else:
268
+ if mode == 'preview':
269
+ mode = 'list'
270
+ sys.stdout.write('\033[2J\033[H')
271
+ else:
272
+ context['output'] = "Cancelled."
273
+ break
274
+ continue
275
+
276
+ if c == 'q' or c == '\x03':
277
+ context['output'] = "Cancelled."
278
+ break
279
+ elif c == 'k':
280
+ if mode == 'list' and selected_jinx > 0:
281
+ selected_jinx -= 1
282
+ elif mode == 'preview' and preview_scroll > 0:
283
+ preview_scroll -= 1
284
+ elif c == 'j':
285
+ if mode == 'list' and selected_jinx < len(current_jinxs) - 1:
286
+ selected_jinx += 1
287
+ elif mode == 'preview' and preview_scroll < 50:
288
+ preview_scroll += 1
289
+ elif c == 'h' and mode == 'list':
290
+ if selected_folder > 0:
291
+ selected_folder -= 1
292
+ current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
293
+ selected_jinx = 0
294
+ jinx_scroll = 0
295
+ elif c == 'l' and mode == 'list':
296
+ if selected_folder < len(folders) - 1:
297
+ selected_folder += 1
298
+ current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
299
+ selected_jinx = 0
300
+ jinx_scroll = 0
301
+ elif c == 'f' and mode == 'list':
302
+ # Toggle filter - cycle through common filters or clear
303
+ if not filter_text:
304
+ filter_text = 'search'
305
+ elif filter_text == 'search':
306
+ filter_text = 'core'
307
+ elif filter_text == 'core':
308
+ filter_text = 'browser'
309
+ else:
310
+ filter_text = ''
311
+ current_jinxs = filter_jinxs(get_jinxs_in_folder(folders[selected_folder]), filter_text)
312
+ selected_jinx = 0
313
+ jinx_scroll = 0
314
+ elif c == 'p' and mode == 'list' and current_jinxs:
315
+ mode = 'preview'
316
+ preview_scroll = 0
317
+ sys.stdout.write('\033[2J\033[H')
318
+ elif c == 'b' and mode == 'preview':
319
+ mode = 'list'
320
+ sys.stdout.write('\033[2J\033[H')
321
+ elif c in ('\r', '\n') and current_jinxs:
322
+ j = current_jinxs[selected_jinx]
323
+ context['output'] = f"Run: /{j['name']}\n\nDescription: {j['description']}"
324
+ context['selected_jinx'] = j
325
+ break
326
+
327
+ finally:
328
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
329
+ sys.stdout.write('\033[?25h')
330
+ sys.stdout.write('\033[2J\033[H')
331
+ sys.stdout.flush()