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
@@ -1,327 +0,0 @@
1
- jinx_name: team_tui
2
- description: Interactive TUI for managing team context, NPCs, and jinxs
3
- inputs: []
4
- steps:
5
- - name: team_manager
6
- engine: python
7
- code: |
8
- import os
9
- import sys
10
- import tty
11
- import termios
12
- import select
13
- import yaml
14
- from pathlib import Path
15
-
16
- if not sys.stdin.isatty():
17
- context['output'] = "Team TUI requires an interactive terminal."
18
- return
19
-
20
- team = context.get('team')
21
- if not team:
22
- context['output'] = "No team loaded."
23
- return
24
-
25
- # ========== State ==========
26
- class TeamState:
27
- def __init__(self):
28
- self.tab = 0 # 0=Team Context, 1=NPCs, 2=Jinxs
29
- self.tabs = ['Team Context', 'NPCs', 'Jinxs']
30
- self.selected_idx = 0
31
- self.scroll_offset = 0
32
- self.editing = False
33
- self.edit_field = None
34
- self.edit_buffer = ""
35
- self.edit_cursor = 0
36
- self.status = ""
37
- self.team_ctx = {}
38
- self.npcs = []
39
- self.jinxs = []
40
-
41
- state = TeamState()
42
-
43
- # ========== Helpers ==========
44
- def get_size():
45
- try:
46
- s = os.get_terminal_size()
47
- return s.columns, s.lines
48
- except:
49
- return 80, 24
50
-
51
- def load_team_data():
52
- """Load team context, NPCs, and jinxs."""
53
- # Load team.ctx
54
- if hasattr(team, 'team_ctx'):
55
- state.team_ctx = team.team_ctx or {}
56
- else:
57
- ctx_path = Path(team.team_path) / 'team.ctx'
58
- if ctx_path.exists():
59
- with open(ctx_path) as f:
60
- state.team_ctx = yaml.safe_load(f) or {}
61
-
62
- # Load NPCs
63
- if hasattr(team, 'npcs'):
64
- state.npcs = list(team.npcs.keys())
65
- else:
66
- npc_dir = Path(team.team_path)
67
- state.npcs = [f.stem for f in npc_dir.glob('*.npc')]
68
-
69
- # Load Jinxs count by folder
70
- jinxs_dir = Path(team.team_path) / 'jinxs'
71
- state.jinxs = []
72
- if jinxs_dir.exists():
73
- for subdir in sorted(jinxs_dir.iterdir()):
74
- if subdir.is_dir():
75
- count = len(list(subdir.glob('*.jinx')))
76
- state.jinxs.append((subdir.name, count))
77
-
78
- def save_team_ctx():
79
- """Save team.ctx file."""
80
- ctx_path = Path(team.team_path) / 'team.ctx'
81
- with open(ctx_path, 'w') as f:
82
- yaml.dump(state.team_ctx, f, default_flow_style=False)
83
- state.status = "Team context saved!"
84
-
85
- # ========== Rendering ==========
86
- def render_screen():
87
- width, height = get_size()
88
- out = []
89
- out.append("\033[2J\033[H")
90
-
91
- # Header
92
- team_name = getattr(team, 'name', 'Unknown')
93
- header = f" Team: {team_name} "
94
- out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
95
- out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;1m{header}\033[0m")
96
-
97
- # Tabs
98
- tab_str = ""
99
- for i, tab in enumerate(state.tabs):
100
- if i == state.tab:
101
- tab_str += f"\033[47;30m [{tab}] \033[0m"
102
- else:
103
- tab_str += f" [{tab}] "
104
- out.append(f"\033[2;2H{tab_str}")
105
-
106
- # Content area
107
- out.append(f"\033[3;1H\033[90m{'─' * width}\033[0m")
108
-
109
- if state.tab == 0:
110
- render_team_ctx(out, width, height)
111
- elif state.tab == 1:
112
- render_npcs(out, width, height)
113
- elif state.tab == 2:
114
- render_jinxs(out, width, height)
115
-
116
- # Status
117
- if state.status:
118
- out.append(f"\033[{height-2};2H\033[33m{state.status}\033[0m")
119
-
120
- # Footer
121
- if state.editing:
122
- footer = "[Enter] Save [Esc] Cancel"
123
- else:
124
- footer = "[Tab] Switch Tab [j/k] Navigate [e] Edit [s] Save [q] Quit"
125
- out.append(f"\033[{height};1H\033[90m{footer[:width]}\033[0m")
126
-
127
- sys.stdout.write(''.join(out))
128
- sys.stdout.flush()
129
-
130
- def render_team_ctx(out, width, height):
131
- """Render team context tab."""
132
- fields = [
133
- ('forenpc', 'Forenpc'),
134
- ('model', 'Model'),
135
- ('provider', 'Provider'),
136
- ('context', 'Context'),
137
- ]
138
-
139
- row = 5
140
- for i, (key, label) in enumerate(fields):
141
- value = state.team_ctx.get(key, '')
142
- if isinstance(value, str) and len(value) > 50:
143
- value = value[:50] + '...'
144
-
145
- if i == state.selected_idx:
146
- if state.editing:
147
- out.append(f"\033[{row};4H\033[1m{label}:\033[0m")
148
- out.append(f"\033[{row+1};6H{state.edit_buffer[:width-10]}\033[7m \033[0m")
149
- row += 2
150
- else:
151
- out.append(f"\033[{row};4H\033[47;30m{label}: {value}\033[0m")
152
- row += 1
153
- else:
154
- if value:
155
- out.append(f"\033[{row};4H{label}: \033[32m{value}\033[0m")
156
- else:
157
- out.append(f"\033[{row};4H{label}: \033[90m(not set)\033[0m")
158
- row += 1
159
-
160
- def render_npcs(out, width, height):
161
- """Render NPCs tab."""
162
- visible_height = height - 8
163
- visible = state.npcs[state.scroll_offset:state.scroll_offset + visible_height]
164
-
165
- row = 5
166
- for i, npc_name in enumerate(visible):
167
- idx = i + state.scroll_offset
168
- npc_obj = team.npcs.get(npc_name) if hasattr(team, 'npcs') else None
169
-
170
- if npc_obj:
171
- model_info = f"{npc_obj.model or 'default'}/{npc_obj.provider or 'default'}"
172
- else:
173
- model_info = ""
174
-
175
- if idx == state.selected_idx:
176
- out.append(f"\033[{row};4H\033[47;30m> {npc_name:<15} {model_info}\033[0m")
177
- else:
178
- out.append(f"\033[{row};4H {npc_name:<15} \033[90m{model_info}\033[0m")
179
- row += 1
180
-
181
- if not state.npcs:
182
- out.append(f"\033[5;4H\033[90mNo NPCs found in team.\033[0m")
183
-
184
- def render_jinxs(out, width, height):
185
- """Render Jinxs tab."""
186
- visible_height = height - 8
187
- visible = state.jinxs[state.scroll_offset:state.scroll_offset + visible_height]
188
-
189
- row = 5
190
- for i, (folder, count) in enumerate(visible):
191
- idx = i + state.scroll_offset
192
- if idx == state.selected_idx:
193
- out.append(f"\033[{row};4H\033[47;30m> {folder}/ ({count} jinxs)\033[0m")
194
- else:
195
- out.append(f"\033[{row};4H {folder}/ \033[90m({count} jinxs)\033[0m")
196
- row += 1
197
-
198
- if not state.jinxs:
199
- out.append(f"\033[5;4H\033[90mNo jinxs folders found.\033[0m")
200
-
201
- # ========== Input Handling ==========
202
- def handle_input(c):
203
- if state.editing:
204
- return handle_edit_input(c)
205
-
206
- if c == 'q':
207
- return False
208
-
209
- if c == '\t': # Tab - switch tabs
210
- state.tab = (state.tab + 1) % len(state.tabs)
211
- state.selected_idx = 0
212
- state.scroll_offset = 0
213
- state.status = ""
214
-
215
- elif c == '\x1b': # Escape sequence
216
- if select.select([sys.stdin], [], [], 0.05)[0]:
217
- c2 = sys.stdin.read(1)
218
- if c2 == '[':
219
- c3 = sys.stdin.read(1)
220
- if c3 == 'A': # Up
221
- move_up()
222
- elif c3 == 'B': # Down
223
- move_down()
224
-
225
- elif c == 'k':
226
- move_up()
227
- elif c == 'j':
228
- move_down()
229
- elif c == 'e' or c == '\r' or c == '\n':
230
- start_edit()
231
- elif c == 's':
232
- save_team_ctx()
233
-
234
- return True
235
-
236
- def handle_edit_input(c):
237
- if c == '\x1b': # Escape - cancel
238
- state.editing = False
239
- state.edit_buffer = ""
240
- state.status = "Edit cancelled"
241
- return True
242
-
243
- if c == '\r' or c == '\n': # Enter - save
244
- if state.edit_field:
245
- state.team_ctx[state.edit_field] = state.edit_buffer
246
- state.status = f"Updated {state.edit_field}"
247
- state.editing = False
248
- state.edit_buffer = ""
249
- state.edit_field = None
250
- return True
251
-
252
- if c == '\x7f' or c == '\x08': # Backspace
253
- if state.edit_cursor > 0:
254
- state.edit_buffer = state.edit_buffer[:state.edit_cursor-1] + state.edit_buffer[state.edit_cursor:]
255
- state.edit_cursor -= 1
256
-
257
- elif c >= ' ' and c <= '~': # Printable
258
- state.edit_buffer = state.edit_buffer[:state.edit_cursor] + c + state.edit_buffer[state.edit_cursor:]
259
- state.edit_cursor += 1
260
-
261
- return True
262
-
263
- def move_up():
264
- state.selected_idx = max(0, state.selected_idx - 1)
265
- if state.selected_idx < state.scroll_offset:
266
- state.scroll_offset = state.selected_idx
267
- state.status = ""
268
-
269
- def move_down():
270
- _, height = get_size()
271
- visible_height = height - 8
272
-
273
- if state.tab == 0:
274
- max_idx = 3 # 4 fields in team ctx
275
- elif state.tab == 1:
276
- max_idx = len(state.npcs) - 1
277
- else:
278
- max_idx = len(state.jinxs) - 1
279
-
280
- state.selected_idx = min(max_idx, state.selected_idx + 1)
281
- if state.selected_idx >= state.scroll_offset + visible_height:
282
- state.scroll_offset = state.selected_idx - visible_height + 1
283
- state.status = ""
284
-
285
- def start_edit():
286
- if state.tab == 0:
287
- fields = ['forenpc', 'model', 'provider', 'context']
288
- if state.selected_idx < len(fields):
289
- state.edit_field = fields[state.selected_idx]
290
- state.edit_buffer = str(state.team_ctx.get(state.edit_field, ''))
291
- state.edit_cursor = len(state.edit_buffer)
292
- state.editing = True
293
- state.status = "Editing..."
294
- elif state.tab == 1:
295
- if state.npcs and state.selected_idx < len(state.npcs):
296
- npc_name = state.npcs[state.selected_idx]
297
- state.status = f"Selected NPC: {npc_name} (edit NPC files directly)"
298
- elif state.tab == 2:
299
- if state.jinxs and state.selected_idx < len(state.jinxs):
300
- folder, _ = state.jinxs[state.selected_idx]
301
- state.status = f"Selected folder: {folder}/"
302
-
303
- # ========== Main Loop ==========
304
- load_team_data()
305
-
306
- fd = sys.stdin.fileno()
307
- old_settings = termios.tcgetattr(fd)
308
-
309
- try:
310
- tty.setcbreak(fd)
311
- sys.stdout.write('\033[?25l') # Hide cursor
312
-
313
- render_screen()
314
-
315
- while True:
316
- c = sys.stdin.read(1)
317
- if not handle_input(c):
318
- break
319
- render_screen()
320
-
321
- finally:
322
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
323
- sys.stdout.write('\033[?25h') # Show cursor
324
- sys.stdout.write('\033[2J\033[H') # Clear screen
325
- sys.stdout.flush()
326
-
327
- context['output'] = "Team manager closed."
@@ -1,331 +0,0 @@
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()