npcsh 1.1.21__py3-none-any.whl → 1.1.23__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 (188) hide show
  1. npcsh/_state.py +282 -125
  2. npcsh/benchmark/npcsh_agent.py +77 -232
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
  4. npcsh/config.py +5 -2
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +8 -6
  7. npcsh/npc_team/corca.npc +5 -12
  8. npcsh/npc_team/frederic.npc +6 -9
  9. npcsh/npc_team/guac.npc +4 -4
  10. npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
  12. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  13. npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
  14. npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
  15. npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
  16. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
  17. npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
  19. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  20. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
  21. npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
  22. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  23. npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
  24. npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
  25. npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
  26. npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
  27. npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
  28. npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
  29. npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
  30. npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
  31. npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
  32. npcsh/npc_team/kadiefa.npc +6 -6
  33. npcsh/npc_team/npcsh.ctx +16 -0
  34. npcsh/npc_team/plonk.npc +5 -9
  35. npcsh/npc_team/sibiji.npc +15 -7
  36. npcsh/npcsh.py +1 -0
  37. npcsh/routes.py +0 -4
  38. npcsh/yap.py +22 -4
  39. npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
  40. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
  41. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
  42. npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
  43. npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
  44. npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
  45. npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
  46. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
  47. npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
  48. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
  49. npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  50. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
  51. npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
  52. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
  53. npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
  54. npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
  55. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
  56. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
  57. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
  58. npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
  59. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
  60. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
  61. npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
  62. npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
  63. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
  64. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
  65. npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
  66. npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
  67. npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
  68. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
  69. npcsh-1.1.23.dist-info/RECORD +216 -0
  70. npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
  71. npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
  72. npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
  73. npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
  74. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
  75. npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
  76. npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
  77. npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
  78. npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
  79. npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
  80. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
  81. npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
  82. npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
  83. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
  84. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
  85. npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
  86. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
  87. npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
  88. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
  89. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  90. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  91. npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
  92. npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
  93. npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
  94. npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
  95. npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
  96. npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
  97. npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
  98. npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  99. npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
  100. npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
  101. npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
  102. npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
  103. npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
  104. npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
  105. npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
  106. npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
  107. npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
  108. npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
  109. npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
  110. npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
  111. npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
  112. npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
  113. npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
  114. npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
  115. npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
  116. npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
  117. npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
  118. npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
  119. npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
  120. npcsh-1.1.21.dist-info/RECORD +0 -243
  121. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  122. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  123. /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
  124. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
  125. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  126. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  127. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  128. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  129. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
  130. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
  131. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  132. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  133. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
  134. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
  135. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
  136. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
  137. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  138. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  139. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
  140. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
  141. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
  142. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
  143. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  144. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  145. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  146. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  147. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  148. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
  149. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
  150. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  151. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
  152. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  153. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
  154. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
  155. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
  156. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
  157. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  158. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
  159. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
  160. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  161. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
  162. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  163. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
  164. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
  165. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
  166. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
  167. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  168. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
  169. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
  170. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
  171. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
  172. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
  173. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
  174. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
  175. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  181. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
  182. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
  183. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  184. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
  185. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
  186. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
  187. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
  188. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,621 @@
1
+ jinx_name: skills
2
+ description: Interactive TUI for browsing, viewing, editing, and using skills
3
+ interactive: true
4
+ inputs: []
5
+ steps:
6
+ - name: skills_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
+ import re
16
+ import json
17
+ import base64
18
+ from pathlib import Path
19
+
20
+ if not sys.stdin.isatty():
21
+ context['output'] = "Skills browser requires an interactive terminal."
22
+
23
+ elif not state or not state.team:
24
+ context['output'] = "No team loaded."
25
+
26
+ else:
27
+ TEAM_DIR = Path(state.team.team_path)
28
+
29
+ class SkillData:
30
+ def __init__(self, name, description, sections, source_path, fmt):
31
+ self.name = name
32
+ self.description = description
33
+ self.sections = sections # dict of {section_name: content}
34
+ self.source_path = source_path
35
+ self.fmt = fmt # 'md' or 'jinx'
36
+
37
+ class TUIState:
38
+ def __init__(self):
39
+ self.tab = 0
40
+ self.tabs = ['Skills', 'Sections', 'Content']
41
+ self.sel = 0
42
+ self.scroll = 0
43
+ self.skills = [] # list of SkillData
44
+ self.cur_skill = None # currently selected SkillData
45
+ self.sec_sel = 0 # section selection index
46
+ self.sec_scroll = 0
47
+ self.status = ""
48
+ self.editing = False
49
+ self.edit_buf = ""
50
+ self.edit_lines = [] # for multiline content editing
51
+ self.edit_line = 0 # current line in editor
52
+ self.content_scroll = 0
53
+
54
+ ui = TUIState()
55
+
56
+ def term_size():
57
+ try:
58
+ s = os.get_terminal_size()
59
+ return s.columns, s.lines
60
+ except:
61
+ return 80, 24
62
+
63
+ def parse_skill_md(path):
64
+ """Parse a SKILL.md file into name, description, sections."""
65
+ text = path.read_text()
66
+ frontmatter = {}
67
+ body = text
68
+ if text.startswith('---'):
69
+ parts = text.split('---', 2)
70
+ if len(parts) >= 3:
71
+ try:
72
+ frontmatter = yaml.safe_load(parts[1]) or {}
73
+ except:
74
+ pass
75
+ body = parts[2]
76
+ name = frontmatter.get('name', path.parent.name)
77
+ desc = frontmatter.get('description', '')
78
+ sections = {}
79
+ current = None
80
+ buf = []
81
+ for line in body.splitlines():
82
+ m = re.match(r'^##\s+(.+)', line)
83
+ if m:
84
+ if current:
85
+ sections[current] = '\n'.join(buf).strip()
86
+ current = m.group(1).strip()
87
+ buf = []
88
+ elif current is not None:
89
+ buf.append(line)
90
+ # skip lines before first ##
91
+ if current:
92
+ sections[current] = '\n'.join(buf).strip()
93
+ return SkillData(name, desc, sections, str(path), 'md')
94
+
95
+ def parse_skill_jinx(path):
96
+ """Parse a .jinx skill file."""
97
+ try:
98
+ data = yaml.safe_load(path.read_text()) or {}
99
+ except:
100
+ return None
101
+ name = data.get('jinx_name', path.stem)
102
+ desc = data.get('description', '')
103
+ sections = {}
104
+ steps = data.get('steps', [])
105
+ for step in steps:
106
+ if step.get('engine') == 'skill':
107
+ raw = step.get('sections', {})
108
+ if isinstance(raw, dict):
109
+ sections = raw
110
+ elif isinstance(raw, str):
111
+ try:
112
+ sections = json.loads(base64.b64decode(raw).decode('utf-8'))
113
+ except:
114
+ try:
115
+ sections = json.loads(raw)
116
+ except:
117
+ pass
118
+ break
119
+ if not sections:
120
+ return None
121
+ return SkillData(name, desc, sections, str(path), 'jinx')
122
+
123
+ def load_skills():
124
+ ui.skills = []
125
+ # Scan jinxs directory for skills
126
+ jdir = TEAM_DIR / 'jinxs'
127
+ if not jdir.exists():
128
+ return
129
+ for root, dirs, files in os.walk(str(jdir)):
130
+ for fname in files:
131
+ fpath = Path(root) / fname
132
+ if fname == 'SKILL.md':
133
+ try:
134
+ sk = parse_skill_md(fpath)
135
+ if sk:
136
+ ui.skills.append(sk)
137
+ except:
138
+ pass
139
+ elif fname.endswith('.jinx'):
140
+ try:
141
+ sk = parse_skill_jinx(fpath)
142
+ if sk:
143
+ ui.skills.append(sk)
144
+ except:
145
+ pass
146
+ # Also load from SKILLS_DIRECTORY if set
147
+ ctx_data = {}
148
+ for cf in TEAM_DIR.glob('*.ctx'):
149
+ try:
150
+ ctx_data = yaml.safe_load(cf.read_text()) or {}
151
+ except:
152
+ pass
153
+ break
154
+ ext_dir = ctx_data.get('SKILLS_DIRECTORY')
155
+ if ext_dir:
156
+ ext_path = Path(os.path.expanduser(ext_dir))
157
+ if not ext_path.is_absolute():
158
+ ext_path = TEAM_DIR / ext_path
159
+ if ext_path.exists():
160
+ for root, dirs, files in os.walk(str(ext_path)):
161
+ for fname in files:
162
+ fpath = Path(root) / fname
163
+ if fname == 'SKILL.md':
164
+ try:
165
+ sk = parse_skill_md(fpath)
166
+ if sk:
167
+ ui.skills.append(sk)
168
+ except:
169
+ pass
170
+ elif fname.endswith('.jinx'):
171
+ try:
172
+ sk = parse_skill_jinx(fpath)
173
+ if sk:
174
+ ui.skills.append(sk)
175
+ except:
176
+ pass
177
+ ui.skills.sort(key=lambda s: s.name)
178
+
179
+ def cur_sections():
180
+ if ui.cur_skill:
181
+ return list(ui.cur_skill.sections.keys())
182
+ return []
183
+
184
+ def cur_content():
185
+ if ui.cur_skill:
186
+ secs = cur_sections()
187
+ if ui.sec_sel < len(secs):
188
+ return ui.cur_skill.sections[secs[ui.sec_sel]]
189
+ return ""
190
+
191
+ def save_skill(skill):
192
+ """Save skill back to disk."""
193
+ p = Path(skill.source_path)
194
+ if skill.fmt == 'md':
195
+ lines = ['---']
196
+ if skill.description:
197
+ lines.append(f'description: {skill.description}')
198
+ lines.append('---')
199
+ lines.append(f'# {skill.name}')
200
+ lines.append('')
201
+ for sec_name, sec_content in skill.sections.items():
202
+ lines.append(f'## {sec_name}')
203
+ lines.append(sec_content)
204
+ lines.append('')
205
+ p.write_text('\n'.join(lines))
206
+ elif skill.fmt == 'jinx':
207
+ data = yaml.safe_load(p.read_text()) or {}
208
+ for step in data.get('steps', []):
209
+ if step.get('engine') == 'skill':
210
+ step['sections'] = skill.sections
211
+ break
212
+ with open(p, 'w') as f:
213
+ yaml.dump(data, f, default_flow_style=False)
214
+ ui.status = f"Saved {skill.name}"
215
+
216
+ # ── rendering ──────────────────────────────────────────
217
+ def w(row, col, text):
218
+ return f"\033[{row};1H\033[K\033[{row};{col}H{text}"
219
+
220
+ def wline(row, text):
221
+ return f"\033[{row};1H\033[K{text}"
222
+
223
+ def render():
224
+ W, H = term_size()
225
+ out = ["\033[H"]
226
+
227
+ # header
228
+ hdr = " Skills "
229
+ pad = '=' * W
230
+ out.append(wline(1, f"\033[7;1m{pad}\033[0m"))
231
+ out.append(f"\033[1;{max(1,(W - len(hdr)) // 2)}H\033[7;1m{hdr}\033[0m")
232
+
233
+ # tabs
234
+ tb = ""
235
+ for i, t in enumerate(ui.tabs):
236
+ if i == ui.tab:
237
+ tb += f"\033[7;1m [{t}] \033[0m"
238
+ else:
239
+ tb += f" {t} "
240
+ out.append(wline(2, f" {tb}"))
241
+ out.append(wline(3, f"\033[90m{'─' * W}\033[0m"))
242
+
243
+ body_start = 4
244
+ body_end = H - 3
245
+ body_h = body_end - body_start + 1
246
+
247
+ if ui.tab == 0:
248
+ render_skills(out, W, body_start, body_h)
249
+ elif ui.tab == 1:
250
+ render_sections(out, W, body_start, body_h)
251
+ else:
252
+ render_content(out, W, body_start, body_h)
253
+
254
+ # separator
255
+ out.append(wline(H - 2, f"\033[90m{'─' * W}\033[0m"))
256
+
257
+ # status
258
+ if ui.status:
259
+ out.append(wline(H - 1, f" \033[33m{ui.status[:W-2]}\033[0m"))
260
+ else:
261
+ out.append(wline(H - 1, ""))
262
+
263
+ # footer
264
+ if ui.editing:
265
+ foot = " [Enter] Next line [Ctrl-D] Done [Esc] Cancel [Backspace] Delete"
266
+ elif ui.tab == 0:
267
+ foot = " [Tab] Switch [j/k] Nav [Enter] View sections [u] Use [c] Critique [q] Quit"
268
+ elif ui.tab == 1:
269
+ foot = " [Tab] Switch [j/k] Nav [Enter] View content [e] Edit [u] Use [q] Back"
270
+ else:
271
+ foot = " [Tab] Switch [j/k] Scroll [e] Edit [u] Use in convo [s] Save [q] Back"
272
+ out.append(wline(H, f"\033[7m{foot[:W].ljust(W)}\033[0m"))
273
+
274
+ sys.stdout.write(''.join(out))
275
+ sys.stdout.flush()
276
+
277
+ def render_skills(out, W, start, body_h):
278
+ vis = ui.skills[ui.scroll:ui.scroll + body_h]
279
+ for r in range(body_h):
280
+ row = start + r
281
+ i = r + ui.scroll
282
+ if r >= len(vis):
283
+ out.append(wline(row, ""))
284
+ continue
285
+ sk = vis[r]
286
+ nsec = len(sk.sections)
287
+ desc = sk.description[:W-35] if sk.description else ''
288
+ desc = desc.replace('\n', ' ')
289
+ tag = f"[{sk.fmt}]"
290
+ if i == ui.sel:
291
+ line = f" > {sk.name:<18} {tag:<6} {nsec}s {desc}"
292
+ out.append(wline(row, f"\033[7m{line[:W].ljust(W)}\033[0m"))
293
+ else:
294
+ out.append(wline(row, f" {sk.name:<18} \033[90m{tag:<6} {nsec}s\033[0m \033[36m{desc}\033[0m"))
295
+ if not ui.skills:
296
+ out.append(wline(start, " \033[90mNo skills found.\033[0m"))
297
+ out.append(wline(start + 1, " \033[90mAdd SKILL.md folders or .jinx files to jinxs/skills/\033[0m"))
298
+
299
+ def render_sections(out, W, start, body_h):
300
+ if not ui.cur_skill:
301
+ out.append(wline(start, " \033[90mNo skill selected.\033[0m"))
302
+ for r in range(1, body_h):
303
+ out.append(wline(start + r, ""))
304
+ return
305
+ # skill header
306
+ out.append(wline(start, f" \033[1m{ui.cur_skill.name}\033[0m \033[90m({ui.cur_skill.fmt})\033[0m"))
307
+ if ui.cur_skill.description:
308
+ d = ui.cur_skill.description[:W-4].replace('\n', ' ')
309
+ out.append(wline(start + 1, f" \033[36m{d}\033[0m"))
310
+ else:
311
+ out.append(wline(start + 1, ""))
312
+ out.append(wline(start + 2, f" \033[90m{'─' * (W - 4)}\033[0m"))
313
+ secs = cur_sections()
314
+ sec_start = start + 3
315
+ sec_h = body_h - 3
316
+ vis = secs[ui.sec_scroll:ui.sec_scroll + sec_h]
317
+ for r in range(sec_h):
318
+ row = sec_start + r
319
+ si = r + ui.sec_scroll
320
+ if r >= len(vis):
321
+ out.append(wline(row, ""))
322
+ continue
323
+ sname = vis[r]
324
+ content = ui.cur_skill.sections[sname]
325
+ preview = content.split('\n')[0][:W-30].replace('\n', ' ')
326
+ if si == ui.sec_sel:
327
+ line = f" > {sname:<20} {preview}"
328
+ out.append(wline(row, f"\033[7m{line[:W].ljust(W)}\033[0m"))
329
+ else:
330
+ out.append(wline(row, f" {sname:<20} \033[90m{preview}\033[0m"))
331
+ if not secs:
332
+ out.append(wline(sec_start, " \033[90mNo sections.\033[0m"))
333
+
334
+ def render_content(out, W, start, body_h):
335
+ if not ui.cur_skill:
336
+ out.append(wline(start, " \033[90mNo skill selected.\033[0m"))
337
+ for r in range(1, body_h):
338
+ out.append(wline(start + r, ""))
339
+ return
340
+ secs = cur_sections()
341
+ sec_name = secs[ui.sec_sel] if ui.sec_sel < len(secs) else '?'
342
+ out.append(wline(start, f" \033[1m{ui.cur_skill.name}\033[0m / \033[1m{sec_name}\033[0m"))
343
+ out.append(wline(start + 1, f" \033[90m{'─' * (W - 4)}\033[0m"))
344
+
345
+ if ui.editing:
346
+ # show edit buffer
347
+ vis = ui.edit_lines[ui.content_scroll:ui.content_scroll + body_h - 2]
348
+ for r in range(body_h - 2):
349
+ row = start + 2 + r
350
+ li = r + ui.content_scroll
351
+ if r >= len(vis):
352
+ out.append(wline(row, ""))
353
+ continue
354
+ line = vis[r]
355
+ if li == ui.edit_line:
356
+ out.append(wline(row, f" \033[7m{line[:W-2].ljust(W-2)}\033[0m"))
357
+ else:
358
+ out.append(wline(row, f" {line[:W-2]}"))
359
+ else:
360
+ content = cur_content()
361
+ lines = content.split('\n')
362
+ vis = lines[ui.content_scroll:ui.content_scroll + body_h - 2]
363
+ for r in range(body_h - 2):
364
+ row = start + 2 + r
365
+ if r >= len(vis):
366
+ out.append(wline(row, ""))
367
+ continue
368
+ out.append(wline(row, f" {vis[r][:W-2]}"))
369
+
370
+ # ── input handling ─────────────────────────────────────
371
+ def handle(c):
372
+ if ui.editing:
373
+ return handle_edit(c)
374
+ if c == '\x1b':
375
+ return handle_esc()
376
+ if c == 'q':
377
+ if ui.tab > 0:
378
+ ui.tab -= 1
379
+ ui.status = ""
380
+ if ui.tab == 0:
381
+ ui.cur_skill = None
382
+ ui.content_scroll = 0
383
+ else:
384
+ return False
385
+ elif c == '\t':
386
+ if ui.tab == 0 and ui.cur_skill:
387
+ ui.tab = 1
388
+ elif ui.tab == 1:
389
+ ui.tab = 2
390
+ ui.content_scroll = 0
391
+ elif ui.tab == 2:
392
+ ui.tab = 0
393
+ ui.content_scroll = 0
394
+ ui.status = ""
395
+ elif c == 'k':
396
+ nav_up()
397
+ elif c == 'j':
398
+ nav_down()
399
+ elif c in ('\r', '\n'):
400
+ do_enter()
401
+ elif c == 'e':
402
+ do_edit()
403
+ elif c == 's':
404
+ do_save()
405
+ elif c == 'u':
406
+ do_use()
407
+ return False # exit TUI to show injected content
408
+ elif c == 'c':
409
+ do_critique()
410
+ return False # exit TUI to run critique
411
+ return True
412
+
413
+ def handle_esc():
414
+ if select.select([fd], [], [], 0.05)[0]:
415
+ c2 = os.read(fd, 1).decode('latin-1')
416
+ if c2 == '[':
417
+ c3 = os.read(fd, 1).decode('latin-1')
418
+ if c3 == 'A':
419
+ nav_up()
420
+ elif c3 == 'B':
421
+ nav_down()
422
+ else:
423
+ if ui.tab > 0:
424
+ ui.tab -= 1
425
+ ui.status = ""
426
+ if ui.tab == 0:
427
+ ui.cur_skill = None
428
+ ui.content_scroll = 0
429
+ return True
430
+
431
+ def handle_edit(c):
432
+ if c == '\x04': # Ctrl-D: done editing
433
+ finish_edit()
434
+ elif c == '\x1b':
435
+ if select.select([fd], [], [], 0.05)[0]:
436
+ c2 = os.read(fd, 1).decode('latin-1')
437
+ if c2 == '[':
438
+ c3 = os.read(fd, 1).decode('latin-1')
439
+ if c3 == 'A': # up
440
+ ui.edit_line = max(0, ui.edit_line - 1)
441
+ if ui.edit_line < ui.content_scroll:
442
+ ui.content_scroll = ui.edit_line
443
+ elif c3 == 'B': # down
444
+ ui.edit_line = min(len(ui.edit_lines) - 1, ui.edit_line + 1)
445
+ _, H = term_size()
446
+ if ui.edit_line >= ui.content_scroll + H - 8:
447
+ ui.content_scroll = ui.edit_line - H + 9
448
+ else:
449
+ # bare Esc: cancel
450
+ ui.editing = False
451
+ ui.edit_lines = []
452
+ ui.status = "Edit cancelled"
453
+ elif c in ('\r', '\n'):
454
+ # insert new line after current
455
+ ui.edit_lines.insert(ui.edit_line + 1, "")
456
+ ui.edit_line += 1
457
+ elif c in ('\x7f', '\x08'):
458
+ line = ui.edit_lines[ui.edit_line]
459
+ if line:
460
+ ui.edit_lines[ui.edit_line] = line[:-1]
461
+ elif ui.edit_line > 0:
462
+ ui.edit_lines.pop(ui.edit_line)
463
+ ui.edit_line -= 1
464
+ elif 32 <= ord(c) <= 126:
465
+ ui.edit_lines[ui.edit_line] += c
466
+ return True
467
+
468
+ def nav_up():
469
+ if ui.tab == 2:
470
+ ui.content_scroll = max(0, ui.content_scroll - 1)
471
+ elif ui.tab == 1:
472
+ ui.sec_sel = max(0, ui.sec_sel - 1)
473
+ if ui.sec_sel < ui.sec_scroll:
474
+ ui.sec_scroll = ui.sec_sel
475
+ else:
476
+ ui.sel = max(0, ui.sel - 1)
477
+ if ui.sel < ui.scroll:
478
+ ui.scroll = ui.sel
479
+ ui.status = ""
480
+
481
+ def nav_down():
482
+ _, H = term_size()
483
+ body_h = H - 6
484
+ if ui.tab == 2:
485
+ content = cur_content()
486
+ maxs = max(0, len(content.split('\n')) - body_h + 2)
487
+ ui.content_scroll = min(maxs, ui.content_scroll + 1)
488
+ elif ui.tab == 1:
489
+ mx = max(0, len(cur_sections()) - 1)
490
+ ui.sec_sel = min(mx, ui.sec_sel + 1)
491
+ if ui.sec_sel >= ui.sec_scroll + body_h - 3:
492
+ ui.sec_scroll = ui.sec_sel - body_h + 4
493
+ else:
494
+ mx = max(0, len(ui.skills) - 1)
495
+ ui.sel = min(mx, ui.sel + 1)
496
+ if ui.sel >= ui.scroll + body_h:
497
+ ui.scroll = ui.sel - body_h + 1
498
+ ui.status = ""
499
+
500
+ def do_enter():
501
+ if ui.tab == 0 and ui.skills:
502
+ ui.cur_skill = ui.skills[ui.sel]
503
+ ui.sec_sel = 0
504
+ ui.sec_scroll = 0
505
+ ui.tab = 1
506
+ ui.status = ""
507
+ elif ui.tab == 1 and ui.cur_skill:
508
+ ui.content_scroll = 0
509
+ ui.tab = 2
510
+ ui.status = ""
511
+
512
+ def do_edit():
513
+ if ui.tab == 2 and ui.cur_skill:
514
+ content = cur_content()
515
+ ui.edit_lines = content.split('\n')
516
+ ui.edit_line = 0
517
+ ui.content_scroll = 0
518
+ ui.editing = True
519
+ ui.status = "Editing — Ctrl-D to save, Esc to cancel"
520
+ elif ui.tab == 1 and ui.cur_skill:
521
+ # enter content view for editing
522
+ ui.tab = 2
523
+ ui.content_scroll = 0
524
+ content = cur_content()
525
+ ui.edit_lines = content.split('\n')
526
+ ui.edit_line = 0
527
+ ui.editing = True
528
+ ui.status = "Editing — Ctrl-D to save, Esc to cancel"
529
+
530
+ def finish_edit():
531
+ if ui.cur_skill:
532
+ secs = cur_sections()
533
+ if ui.sec_sel < len(secs):
534
+ sec_name = secs[ui.sec_sel]
535
+ ui.cur_skill.sections[sec_name] = '\n'.join(ui.edit_lines)
536
+ ui.status = f"Updated {sec_name} — press 's' to save to disk"
537
+ ui.editing = False
538
+ ui.edit_lines = []
539
+
540
+ def do_save():
541
+ if ui.cur_skill:
542
+ save_skill(ui.cur_skill)
543
+
544
+ def do_use():
545
+ """Inject skill content into conversation context."""
546
+ if not ui.cur_skill:
547
+ ui.status = "No skill selected"
548
+ return
549
+ secs = cur_sections()
550
+ if ui.tab == 2 and ui.sec_sel < len(secs):
551
+ sec_name = secs[ui.sec_sel]
552
+ content = ui.cur_skill.sections[sec_name]
553
+ inject = f"[Skill: {ui.cur_skill.name}/{sec_name}]\n{content}"
554
+ elif ui.tab == 1 and ui.sec_sel < len(secs):
555
+ sec_name = secs[ui.sec_sel]
556
+ content = ui.cur_skill.sections[sec_name]
557
+ inject = f"[Skill: {ui.cur_skill.name}/{sec_name}]\n{content}"
558
+ else:
559
+ # inject all sections
560
+ parts = []
561
+ for sn, sc in ui.cur_skill.sections.items():
562
+ parts.append(f"## {sn}\n{sc}")
563
+ inject = f"[Skill: {ui.cur_skill.name}]\n" + '\n\n'.join(parts)
564
+ # Add as a system/context message
565
+ if state and hasattr(state, 'messages'):
566
+ state.messages.append({
567
+ 'role': 'user',
568
+ 'content': f"Use the following skill guidelines:\n\n{inject}"
569
+ })
570
+ context['output'] = f"Injected skill '{ui.cur_skill.name}' into conversation."
571
+
572
+ def do_critique():
573
+ """Queue a critique request for the current skill."""
574
+ if not ui.cur_skill:
575
+ ui.status = "No skill selected"
576
+ return
577
+ parts = []
578
+ for sn, sc in ui.cur_skill.sections.items():
579
+ parts.append(f"## {sn}\n{sc}")
580
+ full = '\n\n'.join(parts)
581
+ prompt = (
582
+ f"Review the following skill definition for '{ui.cur_skill.name}' "
583
+ f"and provide constructive feedback. Check for:\n"
584
+ f"- Completeness: are there missing sections or steps?\n"
585
+ f"- Clarity: is the content clear and actionable?\n"
586
+ f"- Consistency: do sections flow logically?\n"
587
+ f"- Gaps: what edge cases or scenarios are not covered?\n\n"
588
+ f"Skill description: {ui.cur_skill.description}\n\n"
589
+ f"{full}"
590
+ )
591
+ if state and hasattr(state, 'messages'):
592
+ state.messages.append({
593
+ 'role': 'user',
594
+ 'content': prompt
595
+ })
596
+ context['output'] = f"Critiquing skill '{ui.cur_skill.name}'..."
597
+ context['messages'] = state.messages if state else []
598
+
599
+ # ── main loop ──────────────────────────────────────────
600
+ load_skills()
601
+ fd = sys.stdin.fileno()
602
+ old_attrs = termios.tcgetattr(fd)
603
+
604
+ try:
605
+ tty.setcbreak(fd)
606
+ sys.stdout.write('\033[?25l')
607
+ sys.stdout.write('\033[2J\033[H')
608
+ sys.stdout.flush()
609
+ render()
610
+ while True:
611
+ c = os.read(fd, 1).decode('latin-1')
612
+ if not handle(c):
613
+ break
614
+ render()
615
+ finally:
616
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
617
+ sys.stdout.write('\033[?25h\033[2J\033[H')
618
+ sys.stdout.flush()
619
+
620
+ if 'output' not in context:
621
+ context['output'] = "Skills browser closed."