npcsh 1.1.17__py3-none-any.whl → 1.1.19__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 (197) hide show
  1. npcsh/_state.py +122 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +87 -22
  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 +2 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/diff_viewer.py +452 -0
  13. npcsh/execution.py +0 -1
  14. npcsh/guac.py +0 -1
  15. npcsh/mcp_helpers.py +2 -3
  16. npcsh/mcp_server.py +5 -10
  17. npcsh/npc.py +10 -11
  18. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  19. npcsh/npc_team/jinxs/bin/config_tui.jinx +299 -0
  20. npcsh/npc_team/jinxs/bin/memories.jinx +316 -0
  21. npcsh/npc_team/jinxs/bin/setup.jinx +240 -0
  22. npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
  23. npcsh/npc_team/jinxs/bin/team_tui.jinx +327 -0
  24. npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
  25. npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
  26. npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
  27. npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
  28. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
  29. npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
  30. npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
  31. npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
  32. npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
  33. npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
  34. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
  35. npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
  36. npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
  37. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
  38. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
  39. npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
  40. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
  41. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  42. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  43. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  44. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  45. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  46. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  47. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  48. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  49. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  50. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  51. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  52. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  53. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  54. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  55. npcsh/npc_team/jinxs/modes/guac.jinx +542 -0
  56. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  57. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  58. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  59. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  60. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  61. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  62. npcsh/npcsh.py +7 -4
  63. npcsh/plonk.py +0 -1
  64. npcsh/pti.py +0 -1
  65. npcsh/routes.py +1 -3
  66. npcsh/spool.py +0 -1
  67. npcsh/ui.py +0 -1
  68. npcsh/wander.py +0 -1
  69. npcsh/yap.py +0 -1
  70. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
  71. npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  72. npcsh-1.1.19.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  73. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  74. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
  75. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
  76. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +299 -0
  77. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
  78. npcsh-1.1.19.data/data/npcsh/npc_team/corca.jinx +430 -0
  79. npcsh-1.1.19.data/data/npcsh/npc_team/db_search.jinx +348 -0
  80. npcsh-1.1.19.data/data/npcsh/npc_team/file_search.jinx +339 -0
  81. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
  82. npcsh-1.1.19.data/data/npcsh/npc_team/guac.jinx +542 -0
  83. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  84. npcsh-1.1.19.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  85. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
  86. npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  87. npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  88. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
  89. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
  90. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
  91. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
  92. npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  93. npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +379 -0
  94. npcsh-1.1.19.data/data/npcsh/npc_team/pti.jinx +357 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
  96. npcsh-1.1.19.data/data/npcsh/npc_team/reattach.jinx +291 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
  98. npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/send_message.jinx +1 -1
  100. npcsh-1.1.19.data/data/npcsh/npc_team/setup.jinx +240 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  102. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
  103. npcsh-1.1.19.data/data/npcsh/npc_team/spool.jinx +350 -0
  104. npcsh-1.1.19.data/data/npcsh/npc_team/sql.jinx +20 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
  106. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
  107. npcsh-1.1.19.data/data/npcsh/npc_team/sync.jinx +223 -0
  108. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +327 -0
  109. npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +455 -0
  110. npcsh-1.1.19.data/data/npcsh/npc_team/web_search.jinx +283 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
  112. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +13 -7
  113. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
  114. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/METADATA +110 -14
  115. npcsh-1.1.19.dist-info/RECORD +244 -0
  116. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/WHEEL +1 -1
  117. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -3
  118. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  119. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  120. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  121. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  122. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  123. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  124. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  125. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  126. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  127. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  128. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  129. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  130. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  131. npcsh-1.1.17.data/data/npcsh/npc_team/sync.jinx +0 -230
  132. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  133. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  134. npcsh-1.1.17.dist-info/RECORD +0 -219
  135. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  168. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
  169. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
  170. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
  171. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
  172. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  173. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  174. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
  175. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
  176. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
  177. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  178. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
  179. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
  180. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
  181. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
  182. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
  183. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  184. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
  185. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
  186. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
  187. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
  188. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  189. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  190. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  191. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
  192. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  193. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  194. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
  195. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
  196. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
  197. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,223 @@
1
+ jinx_name: "sync"
2
+ description: "Sync npc_team files from the npcsh repo to ~/.npcsh/npc_team. Detects local modifications before overwriting."
3
+ inputs:
4
+ - force: ""
5
+ - dry_run: ""
6
+ - jinxs: ""
7
+ - npcs: ""
8
+ - ctx: ""
9
+ - images: ""
10
+ steps:
11
+ - name: "sync_npc_team"
12
+ engine: "python"
13
+ code: |
14
+ import os
15
+ import hashlib
16
+ import shutil
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+
20
+ force = context.get('force', False)
21
+ dry_run = context.get('dry_run', False)
22
+ sync_jinxs = context.get('jinxs', False)
23
+ sync_npcs = context.get('npcs', False)
24
+ sync_ctx = context.get('ctx', False)
25
+ sync_images = context.get('images', False)
26
+
27
+ # Convert string flags to boolean
28
+ def to_bool(val):
29
+ if isinstance(val, bool):
30
+ return val
31
+ if isinstance(val, str):
32
+ return val.lower() in ('true', '1', 'yes', 'y')
33
+ return bool(val)
34
+
35
+ force = to_bool(force)
36
+ dry_run = to_bool(dry_run)
37
+ sync_jinxs = to_bool(sync_jinxs)
38
+ sync_npcs = to_bool(sync_npcs)
39
+ sync_ctx = to_bool(sync_ctx)
40
+ sync_images = to_bool(sync_images)
41
+
42
+ # If none specified, sync all
43
+ sync_all = not (sync_jinxs or sync_npcs or sync_ctx or sync_images)
44
+
45
+ def get_file_hash(filepath):
46
+ """Get MD5 hash of file contents."""
47
+ try:
48
+ with open(filepath, 'rb') as f:
49
+ return hashlib.md5(f.read()).hexdigest()
50
+ except:
51
+ return None
52
+
53
+ def get_files_recursive(base_path, extensions=None):
54
+ """Get all files recursively, optionally filtered by extensions."""
55
+ files = []
56
+ for root, dirs, filenames in os.walk(base_path):
57
+ # Skip .git directories
58
+ dirs[:] = [d for d in dirs if d != '.git']
59
+ for filename in filenames:
60
+ if filename.startswith('.'):
61
+ continue
62
+ if extensions and not any(filename.endswith(ext) for ext in extensions):
63
+ continue
64
+ full_path = Path(root) / filename
65
+ rel_path = full_path.relative_to(base_path)
66
+ files.append(rel_path)
67
+ return files
68
+
69
+ def do_sync():
70
+ # Find the npc_team directory from the installed npcsh package
71
+ import subprocess
72
+ repo_npc_team = None
73
+
74
+ result = subprocess.run(['pip', 'show', 'npcsh'], capture_output=True, text=True)
75
+ if result.returncode == 0:
76
+ location = None
77
+ editable_location = None
78
+ for line in result.stdout.split('\n'):
79
+ if line.startswith('Location:'):
80
+ location = Path(line.split(':', 1)[1].strip())
81
+ elif line.startswith('Editable project location:'):
82
+ editable_location = Path(line.split(':', 1)[1].strip())
83
+
84
+ # Prefer editable location if available
85
+ if editable_location:
86
+ repo_npc_team = editable_location / "npcsh" / "npc_team"
87
+ elif location:
88
+ repo_npc_team = location / "npcsh" / "npc_team"
89
+
90
+ if not repo_npc_team or not repo_npc_team.exists():
91
+ return f"Error: Could not find npcsh package npc_team directory. Is npcsh installed?"
92
+
93
+ local_npc_team = Path.home() / ".npcsh" / "npc_team"
94
+
95
+ if not local_npc_team.exists():
96
+ return f"Error: Local npc_team directory not found at {local_npc_team}"
97
+
98
+ # Build list of extensions to sync based on flags
99
+ sync_extensions = []
100
+ if sync_all or sync_npcs:
101
+ sync_extensions.append('.npc')
102
+ if sync_all or sync_ctx:
103
+ sync_extensions.append('.ctx')
104
+ if sync_all or sync_jinxs:
105
+ sync_extensions.append('.jinx')
106
+ if sync_all or sync_images:
107
+ sync_extensions.extend(['.png', '.jpg', '.jpeg'])
108
+
109
+ # Get files from repo
110
+ repo_files = get_files_recursive(repo_npc_team, sync_extensions)
111
+
112
+ output_lines = []
113
+ output_lines.append(f"Syncing from: {repo_npc_team}")
114
+ output_lines.append(f"Syncing to: {local_npc_team}")
115
+
116
+ # Show what's being synced
117
+ sync_types = []
118
+ if sync_all:
119
+ sync_types.append("all")
120
+ else:
121
+ if sync_npcs: sync_types.append("npcs")
122
+ if sync_ctx: sync_types.append("ctx")
123
+ if sync_jinxs: sync_types.append("jinxs")
124
+ if sync_images: sync_types.append("images")
125
+ output_lines.append(f"Syncing: {', '.join(sync_types)}")
126
+
127
+ if dry_run:
128
+ output_lines.append("\n[DRY RUN - No changes will be made]\n")
129
+ output_lines.append("")
130
+
131
+ new_files = []
132
+ updated_files = []
133
+ modified_locally = []
134
+ unchanged_files = []
135
+
136
+ for rel_path in repo_files:
137
+ repo_file = repo_npc_team / rel_path
138
+ local_file = local_npc_team / rel_path
139
+
140
+ if not local_file.exists():
141
+ new_files.append(rel_path)
142
+ else:
143
+ repo_hash = get_file_hash(repo_file)
144
+ local_hash = get_file_hash(local_file)
145
+
146
+ if repo_hash == local_hash:
147
+ unchanged_files.append(rel_path)
148
+ else:
149
+ # Check if local file is newer (possibly modified by user)
150
+ repo_mtime = repo_file.stat().st_mtime
151
+ local_mtime = local_file.stat().st_mtime
152
+
153
+ if local_mtime > repo_mtime:
154
+ modified_locally.append((rel_path, local_mtime, repo_mtime))
155
+ else:
156
+ updated_files.append(rel_path)
157
+
158
+ # Report findings
159
+ if new_files:
160
+ output_lines.append(f"New files to add ({len(new_files)}):")
161
+ for f in new_files:
162
+ output_lines.append(f" + {f}")
163
+ output_lines.append("")
164
+
165
+ if updated_files:
166
+ output_lines.append(f"Files to update ({len(updated_files)}):")
167
+ for f in updated_files:
168
+ output_lines.append(f" ~ {f}")
169
+ output_lines.append("")
170
+
171
+ if modified_locally:
172
+ output_lines.append(f"Locally modified files ({len(modified_locally)}):")
173
+ for f, local_t, repo_t in modified_locally:
174
+ local_dt = datetime.fromtimestamp(local_t).strftime('%Y-%m-%d %H:%M')
175
+ repo_dt = datetime.fromtimestamp(repo_t).strftime('%Y-%m-%d %H:%M')
176
+ output_lines.append(f" ! {f}")
177
+ output_lines.append(f" local: {local_dt} repo: {repo_dt}")
178
+ if not force:
179
+ output_lines.append(" (use --force to overwrite these)")
180
+ output_lines.append("")
181
+
182
+ if unchanged_files:
183
+ output_lines.append(f"Already up to date: {len(unchanged_files)} files")
184
+ output_lines.append("")
185
+
186
+ # Perform sync if not dry run
187
+ if not dry_run:
188
+ synced = 0
189
+ skipped = 0
190
+
191
+ # Sync new files
192
+ for rel_path in new_files:
193
+ src = repo_npc_team / rel_path
194
+ dst = local_npc_team / rel_path
195
+ dst.parent.mkdir(parents=True, exist_ok=True)
196
+ shutil.copy2(src, dst)
197
+ synced += 1
198
+
199
+ # Sync updated files
200
+ for rel_path in updated_files:
201
+ src = repo_npc_team / rel_path
202
+ dst = local_npc_team / rel_path
203
+ dst.parent.mkdir(parents=True, exist_ok=True)
204
+ shutil.copy2(src, dst)
205
+ synced += 1
206
+
207
+ # Handle locally modified files
208
+ for rel_path, _, _ in modified_locally:
209
+ if force:
210
+ src = repo_npc_team / rel_path
211
+ dst = local_npc_team / rel_path
212
+ shutil.copy2(src, dst)
213
+ synced += 1
214
+ else:
215
+ skipped += 1
216
+
217
+ output_lines.append(f"Synced: {synced} files")
218
+ if skipped:
219
+ output_lines.append(f"Skipped: {skipped} locally modified files")
220
+
221
+ return "\n".join(output_lines)
222
+
223
+ context['output'] = do_sync()
@@ -0,0 +1,327 @@
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."