npcsh 1.1.20__py3-none-any.whl → 1.1.22__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 (186) hide show
  1. npcsh/_state.py +15 -76
  2. npcsh/benchmark/npcsh_agent.py +22 -14
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +2 -2
  4. npcsh/diff_viewer.py +3 -3
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +12 -6
  7. npcsh/npc_team/corca.npc +0 -1
  8. npcsh/npc_team/frederic.npc +2 -3
  9. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  10. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +83 -61
  11. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +17 -6
  12. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +17 -6
  13. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +52 -14
  14. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  15. npcsh/npc_team/jinxs/{bin → lib/utils}/jinxs.jinx +12 -12
  16. npcsh/npc_team/jinxs/{bin → lib/utils}/models.jinx +7 -7
  17. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +6 -6
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +1633 -295
  19. npcsh/npc_team/jinxs/modes/arxiv.jinx +5 -5
  20. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  21. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  22. npcsh/npc_team/jinxs/modes/convene.jinx +597 -0
  23. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  24. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  25. {npcsh-1.1.20.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/kg.jinx +82 -15
  26. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  27. npcsh/npc_team/jinxs/{bin → modes}/nql.jinx +10 -21
  28. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  29. npcsh/npc_team/jinxs/modes/plonk.jinx +503 -308
  30. npcsh/npc_team/jinxs/modes/reattach.jinx +3 -3
  31. npcsh/npc_team/jinxs/modes/spool.jinx +3 -3
  32. npcsh/npc_team/jinxs/{bin → modes}/team.jinx +12 -12
  33. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  34. npcsh/npc_team/jinxs/modes/wander.jinx +454 -181
  35. npcsh/npc_team/jinxs/modes/yap.jinx +630 -182
  36. npcsh/npc_team/kadiefa.npc +2 -1
  37. npcsh/npc_team/sibiji.npc +3 -3
  38. npcsh/npcsh.py +112 -47
  39. npcsh/routes.py +4 -1
  40. npcsh/salmon_simulation.py +0 -0
  41. npcsh-1.1.22.data/data/npcsh/npc_team/alicanto.jinx +1694 -0
  42. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.npc +12 -6
  43. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/arxiv.jinx +5 -5
  44. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  45. npcsh-1.1.22.data/data/npcsh/npc_team/build.jinx +378 -0
  46. npcsh-1.1.22.data/data/npcsh/npc_team/compress.jinx +428 -0
  47. npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  48. npcsh-1.1.22.data/data/npcsh/npc_team/corca.jinx +820 -0
  49. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.npc +0 -1
  50. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/db_search.jinx +17 -6
  51. npcsh-1.1.22.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  52. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/file_search.jinx +17 -6
  53. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic.npc +2 -3
  54. npcsh-1.1.22.data/data/npcsh/npc_team/git.jinx +795 -0
  55. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/jinxs.jinx +12 -12
  56. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.npc +2 -1
  57. {npcsh/npc_team/jinxs/bin → npcsh-1.1.22.data/data/npcsh/npc_team}/kg.jinx +82 -15
  58. npcsh-1.1.22.data/data/npcsh/npc_team/memories.jinx +414 -0
  59. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/models.jinx +7 -7
  60. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/nql.jinx +10 -21
  61. npcsh-1.1.22.data/data/npcsh/npc_team/papers.jinx +578 -0
  62. npcsh-1.1.22.data/data/npcsh/npc_team/plonk.jinx +574 -0
  63. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/reattach.jinx +3 -3
  64. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/setup.jinx +6 -6
  65. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.npc +3 -3
  66. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.jinx +3 -3
  67. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/team.jinx +12 -12
  68. npcsh-1.1.22.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  69. npcsh-1.1.22.data/data/npcsh/npc_team/wander.jinx +728 -0
  70. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/web_search.jinx +52 -14
  71. npcsh-1.1.22.data/data/npcsh/npc_team/yap.jinx +716 -0
  72. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/METADATA +246 -281
  73. npcsh-1.1.22.dist-info/RECORD +240 -0
  74. npcsh-1.1.22.dist-info/entry_points.txt +11 -0
  75. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -300
  76. npcsh/npc_team/jinxs/bin/memories.jinx +0 -317
  77. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  78. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -418
  79. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  80. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  81. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  82. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  83. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  84. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  85. npcsh/npc_team/plonkjr.npc +0 -23
  86. npcsh-1.1.20.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  87. npcsh-1.1.20.data/data/npcsh/npc_team/build.jinx +0 -65
  88. npcsh-1.1.20.data/data/npcsh/npc_team/compress.jinx +0 -140
  89. npcsh-1.1.20.data/data/npcsh/npc_team/config_tui.jinx +0 -300
  90. npcsh-1.1.20.data/data/npcsh/npc_team/corca.jinx +0 -430
  91. npcsh-1.1.20.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  92. npcsh-1.1.20.data/data/npcsh/npc_team/kg_search.jinx +0 -418
  93. npcsh-1.1.20.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  94. npcsh-1.1.20.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  95. npcsh-1.1.20.data/data/npcsh/npc_team/memories.jinx +0 -317
  96. npcsh-1.1.20.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  97. npcsh-1.1.20.data/data/npcsh/npc_team/plonk.jinx +0 -379
  98. npcsh-1.1.20.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  99. npcsh-1.1.20.data/data/npcsh/npc_team/search.jinx +0 -54
  100. npcsh-1.1.20.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  101. npcsh-1.1.20.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  102. npcsh-1.1.20.data/data/npcsh/npc_team/wander.jinx +0 -455
  103. npcsh-1.1.20.data/data/npcsh/npc_team/yap.jinx +0 -268
  104. npcsh-1.1.20.dist-info/RECORD +0 -248
  105. npcsh-1.1.20.dist-info/entry_points.txt +0 -25
  106. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  107. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  108. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  109. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  110. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  111. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  112. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  113. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  114. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.png +0 -0
  115. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  116. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  117. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/chat.jinx +0 -0
  118. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/click.jinx +0 -0
  119. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  120. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  121. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  122. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  123. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/compile.jinx +0 -0
  124. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  125. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/convene.jinx +0 -0
  126. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.png +0 -0
  127. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca_example.png +0 -0
  128. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  129. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  130. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic4.png +0 -0
  131. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.jinx +0 -0
  132. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.npc +0 -0
  133. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.png +0 -0
  134. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/help.jinx +0 -0
  135. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  136. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/init.jinx +0 -0
  137. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  138. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  139. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  140. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  141. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  142. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  143. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/notify.jinx +0 -0
  144. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  145. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  146. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  147. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  148. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/ots.jinx +0 -0
  149. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/paste.jinx +0 -0
  150. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.npc +0 -0
  151. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.png +0 -0
  152. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  153. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/pti.jinx +0 -0
  154. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/python.jinx +0 -0
  155. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  156. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/roll.jinx +0 -0
  157. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  158. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sample.jinx +0 -0
  159. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  160. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  161. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/serve.jinx +0 -0
  162. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/set.jinx +0 -0
  163. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sh.jinx +0 -0
  164. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/shh.jinx +0 -0
  165. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.png +0 -0
  166. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  167. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  168. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.png +0 -0
  169. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sql.jinx +0 -0
  170. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch.jinx +0 -0
  171. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  172. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  173. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switches.jinx +0 -0
  174. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sync.jinx +0 -0
  175. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/wait.jinx +0 -0
  181. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  182. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/yap.png +0 -0
  183. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  184. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/WHEEL +0 -0
  185. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/licenses/LICENSE +0 -0
  186. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/top_level.txt +0 -0
@@ -417,7 +417,7 @@ steps:
417
417
  header = " ARXIV: '" + query + "' (" + str(len(papers)) + " papers) [sort:" + sort_key + "] "
418
418
  else:
419
419
  header = " " + papers[selected]['title'][:width-4] + " "
420
- sys.stdout.write('\033[45;37;1m' + header.ljust(width) + '\033[0m\n')
420
+ sys.stdout.write('\033[7;1m' + header.ljust(width) + '\033[0m\n')
421
421
 
422
422
  if mode == 'list':
423
423
  # Column headers
@@ -456,7 +456,7 @@ steps:
456
456
  sys.stdout.write(' ' + line)
457
457
 
458
458
  sys.stdout.write('\033[' + str(height-1) + ';1H\033[K\033[90m' + ("─" * width) + '\033[0m')
459
- sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[45;37m j/k:Nav Enter:View o/i/p:Open d:Download 1-3:Sort y/a:Filter c:Clear q:Quit [' + str(selected+1) + '/' + str(len(papers)) + '] \033[0m')
459
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[7m j/k:Nav Enter:View o/i/p:Open d:Download 1-3:Sort y/a:Filter c:Clear q:Quit [' + str(selected+1) + '/' + str(len(papers)) + '] \033[0m')
460
460
 
461
461
  elif mode == 'detail':
462
462
  sys.stdout.write('\033[90m' + ("─" * width) + '\033[0m\n')
@@ -489,13 +489,13 @@ steps:
489
489
  sys.stdout.write(' ' + lines[idx][:width-4])
490
490
 
491
491
  sys.stdout.write('\033[' + str(height-1) + ';1H\033[K\033[90m' + ("─" * width) + '\033[0m')
492
- sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[45;37m j/k:Scroll b:Back o:Browser i:Incognide p:PDF d:Download v:TermView q:Quit \033[0m')
492
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[7m j/k:Scroll b:Back o:Browser i:Incognide p:PDF d:Download v:TermView q:Quit \033[0m')
493
493
 
494
494
  elif mode == 'pdfview':
495
495
  # PDF terminal view mode
496
496
  p = papers[selected]
497
497
  header = " PDF VIEW: " + p['aid'] + " "
498
- sys.stdout.write('\033[46;37;1m' + header.ljust(width) + '\033[0m\n')
498
+ sys.stdout.write('\033[7;1m' + header.ljust(width) + '\033[0m\n')
499
499
  sys.stdout.write('\033[90m' + ("─" * width) + '\033[0m\n')
500
500
 
501
501
  view_height = height - 4
@@ -510,7 +510,7 @@ steps:
510
510
 
511
511
  sys.stdout.write('\033[' + str(height-1) + ';1H\033[K\033[90m' + ("─" * width) + '\033[0m')
512
512
  pct = int((pdf_scroll / max(1, len(pdf_lines) - view_height)) * 100) if len(pdf_lines) > view_height else 100
513
- sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[46;37m j/k/PgDn/PgUp:Scroll b:Back d:Download q:Quit [' + str(pct) + '%] \033[0m')
513
+ sys.stdout.write('\033[' + str(height) + ';1H\033[K\033[7m j/k/PgDn/PgUp:Scroll b:Back d:Download q:Quit [' + str(pct) + '%] \033[0m')
514
514
 
515
515
  sys.stdout.flush()
516
516
 
@@ -0,0 +1,378 @@
1
+ jinx_name: build
2
+ description: Interactive TUI for building deployment artifacts from an NPC team
3
+ interactive: true
4
+ inputs:
5
+ - target: ""
6
+ - outdir: "./build"
7
+ - team: "./npc_team"
8
+ - port: 5337
9
+ - cors: ""
10
+ steps:
11
+ - name: build
12
+ engine: python
13
+ code: |
14
+ import os
15
+ import sys
16
+ import tty
17
+ import termios
18
+ import select as _sel
19
+
20
+ def _resolve_team_path(raw_team):
21
+ """Resolve team path: try given path first, then local ./npc_team, then global ~/.npcsh/npc_team."""
22
+ candidates = []
23
+ if raw_team:
24
+ candidates.append(os.path.abspath(os.path.expanduser(raw_team)))
25
+ local = os.path.abspath('./npc_team')
26
+ global_ = os.path.expanduser('~/.npcsh/npc_team')
27
+ if local not in candidates:
28
+ candidates.append(local)
29
+ if global_ not in candidates:
30
+ candidates.append(global_)
31
+ for p in candidates:
32
+ if os.path.isdir(p):
33
+ if p != candidates[0] and candidates[0] != p:
34
+ print(f"\033[33m⚠ Local team not found at {candidates[0]}, using {p}\033[0m")
35
+ return p
36
+ raise FileNotFoundError(
37
+ f"No npc_team directory found. Searched:\n"
38
+ + "\n".join(f" - {c}" for c in candidates)
39
+ + "\n\nCreate a local npc_team/ or ensure ~/.npcsh/npc_team exists."
40
+ )
41
+
42
+ _direct_target = (context.get('target') or '').strip().lower()
43
+ _direct = bool(_direct_target) or not sys.stdin.isatty()
44
+
45
+ if _direct:
46
+ # Direct build: target passed explicitly or non-interactive
47
+ try:
48
+ from npcpy.build_funcs import (
49
+ build_flask_server as _bf,
50
+ build_docker_compose as _bd,
51
+ build_cli_executable as _bc,
52
+ build_static_site as _bs,
53
+ )
54
+ _target = _direct_target or 'flask'
55
+ _builders = {'flask': _bf, 'docker': _bd, 'cli': _bc, 'static': _bs}
56
+ if _target not in _builders:
57
+ context['output'] = f"Unknown target: {_target}. Available: {list(_builders.keys())}"
58
+ else:
59
+ _cfg = {
60
+ 'team_path': _resolve_team_path(context.get('team')),
61
+ 'output_dir': os.path.abspath(os.path.expanduser(context.get('outdir') or './build')),
62
+ 'target': _target,
63
+ 'port': int(context.get('port') or 5337),
64
+ 'cors_origins': [c.strip() for c in (context.get('cors') or '').split(',') if c.strip()] or None,
65
+ }
66
+ _r = _builders[_target](_cfg)
67
+ context['output'] = _r.get('output', 'Build complete.')
68
+ except ImportError:
69
+ context['output'] = "Build functions not available. Install npcpy with build support."
70
+ except Exception as _e:
71
+ context['output'] = f"Build failed: {_e}"
72
+ context['messages'] = context.get('messages', [])
73
+ exit()
74
+
75
+ try:
76
+ from npcpy.build_funcs import (
77
+ build_flask_server,
78
+ build_docker_compose,
79
+ build_cli_executable,
80
+ build_static_site,
81
+ )
82
+ BUILD_AVAILABLE = True
83
+ except ImportError:
84
+ BUILD_AVAILABLE = False
85
+
86
+ if not BUILD_AVAILABLE:
87
+ context['output'] = "Build functions not available. Install npcpy with build support."
88
+ exit()
89
+
90
+ # ========== State ==========
91
+ class BuildState:
92
+ def __init__(self):
93
+ self.phase = 0 # 0=select target, 1=configure, 2=building, 3=result
94
+ self.sel = 0
95
+ self.targets = [
96
+ {'key': 'flask', 'name': 'Flask Server', 'desc': 'Standalone Python web server with NPC API endpoints'},
97
+ {'key': 'docker', 'name': 'Docker', 'desc': 'Containerized deployment with Dockerfile and docker-compose'},
98
+ {'key': 'cli', 'name': 'CLI Scripts', 'desc': 'Per-NPC executable scripts for direct CLI usage'},
99
+ {'key': 'static', 'name': 'Static Site', 'desc': 'HTML documentation page listing team NPCs'},
100
+ ]
101
+ try:
102
+ _resolved_team = _resolve_team_path(context.get('team'))
103
+ except FileNotFoundError:
104
+ _resolved_team = os.path.expanduser(context.get('team') or './npc_team')
105
+ self.config = {
106
+ 'outdir': os.path.expanduser(context.get('outdir') or './build'),
107
+ 'team': _resolved_team,
108
+ 'port': str(context.get('port') or 5337),
109
+ 'cors': context.get('cors') or '',
110
+ }
111
+ self.config_keys = ['outdir', 'team', 'port', 'cors']
112
+ self.config_labels = {'outdir': 'Output Dir', 'team': 'Team Path', 'port': 'Port', 'cors': 'CORS Origins'}
113
+ self.config_sel = 0
114
+ self.editing = False
115
+ self.edit_buf = ""
116
+ self.edit_key = ""
117
+ self.result = ""
118
+ self.error = ""
119
+ self.files_created = []
120
+
121
+ ui = BuildState()
122
+
123
+ # TUI mode: no target was passed, user picks interactively
124
+
125
+ # ========== Helpers ==========
126
+ def get_size():
127
+ try:
128
+ s = os.get_terminal_size()
129
+ return s.columns, s.lines
130
+ except:
131
+ return 80, 24
132
+
133
+ # ========== Rendering ==========
134
+ def render():
135
+ width, height = get_size()
136
+ out = []
137
+ out.append('\033[2J\033[H')
138
+
139
+ phase_names = ['Select Target', 'Configure', 'Building...', 'Complete']
140
+ header = f' NPC BUILD - {phase_names[ui.phase]} '
141
+ out.append(f'\033[1;1H\033[7;1m{header.ljust(width)}\033[0m')
142
+
143
+ if ui.phase == 0:
144
+ render_targets(out, width, height)
145
+ elif ui.phase == 1:
146
+ render_config(out, width, height)
147
+ elif ui.phase == 2:
148
+ render_building(out, width, height)
149
+ elif ui.phase == 3:
150
+ render_result(out, width, height)
151
+
152
+ if ui.error:
153
+ out.append(f'\033[{height-1};1H\033[K \033[31m{ui.error[:width-3]}\033[0m')
154
+
155
+ sys.stdout.write(''.join(out))
156
+ sys.stdout.flush()
157
+
158
+ def render_targets(out, width, height):
159
+ banner = [
160
+ '\033[33m ██████╗ ██╗ ██╗██╗██╗ ██████╗ \033[0m',
161
+ '\033[33m██╔══██╗██║ ██║██║██║ ██╔══██╗\033[0m',
162
+ '\033[33m██████╔╝██║ ██║██║██║ ██║ ██║\033[0m',
163
+ '\033[33m██╔══██╗██║ ██║██║██║ ██║ ██║\033[0m',
164
+ '\033[33m██████╔╝╚██████╔╝██║███████╗██████╔╝\033[0m',
165
+ '\033[33m╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ \033[0m',
166
+ ]
167
+ for i, line in enumerate(banner):
168
+ out.append(f'\033[{3+i};3H{line}')
169
+
170
+ y = 3 + len(banner) + 1
171
+ out.append(f'\033[{y};3H\033[1mSelect a build target:\033[0m')
172
+ y += 2
173
+
174
+ for i, target in enumerate(ui.targets):
175
+ selected = (i == ui.sel)
176
+ if selected:
177
+ out.append(f'\033[{y};2H\033[7;1m > {target["name"]:<20}\033[0m \033[7m{target["desc"][:width-28]}\033[0m')
178
+ else:
179
+ out.append(f'\033[{y};2H \033[1m{target["name"]:<20}\033[0m \033[90m{target["desc"][:width-28]}\033[0m')
180
+ y += 2
181
+
182
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Navigate Enter:Select q:Quit \033[0m'.ljust(width))
183
+
184
+ def render_config(out, width, height):
185
+ target = ui.targets[ui.sel]
186
+ out.append(f'\033[3;3H\033[1mTarget: \033[36m{target["name"]}\033[0m')
187
+ out.append(f'\033[4;3H\033[90m{target["desc"]}\033[0m')
188
+
189
+ out.append(f'\033[6;3H\033[1mConfiguration:\033[0m')
190
+
191
+ y = 8
192
+ for i, key in enumerate(ui.config_keys):
193
+ label = ui.config_labels[key]
194
+ val = ui.config[key]
195
+ selected = (i == ui.config_sel)
196
+
197
+ if ui.editing and key == ui.edit_key:
198
+ out.append(f'\033[{y};3H\033[33m{label}:\033[0m \033[7m {ui.edit_buf}_ \033[0m')
199
+ elif selected:
200
+ out.append(f'\033[{y};3H\033[7m {label}: {val} \033[0m')
201
+ else:
202
+ out.append(f'\033[{y};3H \033[1m{label}:\033[0m {val}')
203
+ y += 2
204
+
205
+ # Show which configs are relevant for this target
206
+ y += 1
207
+ relevant = {'flask': ['outdir', 'team', 'port', 'cors'],
208
+ 'docker': ['outdir', 'team', 'port', 'cors'],
209
+ 'cli': ['outdir', 'team'],
210
+ 'static': ['outdir', 'team']}
211
+ rel = relevant.get(target['key'], ui.config_keys)
212
+ out.append(f'\033[{y};3H\033[90mRelevant for {target["name"]}: {", ".join(rel)}\033[0m')
213
+
214
+ if ui.editing:
215
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:Save Esc:Cancel \033[0m'.ljust(width))
216
+ else:
217
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Navigate e:Edit Enter:Build Backspace:Back q:Quit \033[0m'.ljust(width))
218
+
219
+ def render_building(out, width, height):
220
+ target = ui.targets[ui.sel]
221
+ mid = height // 2
222
+ out.append(f'\033[{mid};{width//2-10}H\033[33;1mBuilding {target["name"]}...\033[0m')
223
+
224
+ def render_result(out, width, height):
225
+ target = ui.targets[ui.sel]
226
+ y = 3
227
+ if ui.error:
228
+ out.append(f'\033[{y};3H\033[31;1mBuild Failed\033[0m')
229
+ y += 2
230
+ out.append(f'\033[{y};3H\033[31m{ui.error[:width-6]}\033[0m')
231
+ else:
232
+ out.append(f'\033[{y};3H\033[32;1mBuild Complete: {target["name"]}\033[0m')
233
+ y += 2
234
+ for line in ui.result.split('\n'):
235
+ if y >= height - 3:
236
+ break
237
+ out.append(f'\033[{y};3H{line[:width-6]}')
238
+ y += 1
239
+
240
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:New Build o:Open output dir q:Quit \033[0m'.ljust(width))
241
+
242
+ # ========== Build Execution ==========
243
+ def do_build():
244
+ target = ui.targets[ui.sel]
245
+ config = {
246
+ 'team_path': _resolve_team_path(ui.config['team']),
247
+ 'output_dir': os.path.abspath(os.path.expanduser(ui.config['outdir'])),
248
+ 'target': target['key'],
249
+ 'port': int(ui.config['port']),
250
+ 'cors_origins': [c.strip() for c in ui.config['cors'].split(',') if c.strip()] or None,
251
+ }
252
+
253
+ builders = {
254
+ 'flask': build_flask_server,
255
+ 'docker': build_docker_compose,
256
+ 'cli': build_cli_executable,
257
+ 'static': build_static_site,
258
+ }
259
+
260
+ try:
261
+ result = builders[target['key']](config)
262
+ ui.result = result.get('output', 'Build complete.')
263
+ ui.error = ""
264
+ except Exception as e:
265
+ ui.error = str(e)
266
+ ui.result = ""
267
+
268
+ ui.phase = 3
269
+
270
+ # ========== Input ==========
271
+ def handle_input(c, fd):
272
+ if ui.editing:
273
+ return handle_edit(c, fd)
274
+
275
+ if c == '\x1b':
276
+ if _sel.select([fd], [], [], 0.05)[0]:
277
+ c2 = os.read(fd, 1).decode('latin-1')
278
+ if c2 == '[':
279
+ c3 = os.read(fd, 1).decode('latin-1')
280
+ if c3 == 'A': move_up()
281
+ elif c3 == 'B': move_down()
282
+ return True
283
+
284
+ if c == 'q' or c == '\x03':
285
+ return False
286
+
287
+ if ui.phase == 0:
288
+ if c == 'j': move_down()
289
+ elif c == 'k': move_up()
290
+ elif c in ('\r', '\n'):
291
+ ui.phase = 1
292
+ ui.config_sel = 0
293
+ elif ui.phase == 1:
294
+ if c == 'j': move_down()
295
+ elif c == 'k': move_up()
296
+ elif c == 'e':
297
+ key = ui.config_keys[ui.config_sel]
298
+ ui.editing = True
299
+ ui.edit_key = key
300
+ ui.edit_buf = ui.config[key]
301
+ elif c == '\x7f' or c == '\x08':
302
+ ui.phase = 0
303
+ ui.config_sel = 0
304
+ elif c in ('\r', '\n'):
305
+ ui.phase = 2
306
+ render()
307
+ do_build()
308
+ elif ui.phase == 3:
309
+ if c in ('\r', '\n'):
310
+ ui.phase = 0
311
+ ui.sel = 0
312
+ ui.error = ""
313
+ ui.result = ""
314
+ elif c == 'o':
315
+ outdir = os.path.abspath(os.path.expanduser(ui.config['outdir']))
316
+ if os.path.isdir(outdir):
317
+ import subprocess
318
+ try:
319
+ subprocess.Popen(['xdg-open', outdir], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
320
+ except:
321
+ pass
322
+ return True
323
+
324
+ def handle_edit(c, fd):
325
+ if c == '\x1b':
326
+ if _sel.select([fd], [], [], 0.05)[0]:
327
+ os.read(fd, 2)
328
+ ui.editing = False
329
+ ui.edit_buf = ""
330
+ return True
331
+ if c in ('\r', '\n'):
332
+ ui.config[ui.edit_key] = ui.edit_buf
333
+ ui.editing = False
334
+ return True
335
+ if c == '\x7f' or c == '\x08':
336
+ ui.edit_buf = ui.edit_buf[:-1]
337
+ return True
338
+ if c >= ' ' and c <= '~':
339
+ ui.edit_buf += c
340
+ return True
341
+
342
+ def move_up():
343
+ if ui.phase == 0:
344
+ ui.sel = max(0, ui.sel - 1)
345
+ elif ui.phase == 1:
346
+ ui.config_sel = max(0, ui.config_sel - 1)
347
+
348
+ def move_down():
349
+ if ui.phase == 0:
350
+ ui.sel = min(len(ui.targets) - 1, ui.sel + 1)
351
+ elif ui.phase == 1:
352
+ ui.config_sel = min(len(ui.config_keys) - 1, ui.config_sel + 1)
353
+
354
+ # ========== Main Loop ==========
355
+ fd = sys.stdin.fileno()
356
+ old_settings = termios.tcgetattr(fd)
357
+
358
+ try:
359
+ tty.setcbreak(fd)
360
+ sys.stdout.write('\033[?25l')
361
+ render()
362
+
363
+ running = True
364
+ while running:
365
+ if _sel.select([fd], [], [], 0.5)[0]:
366
+ c = os.read(fd, 1).decode('latin-1')
367
+ running = handle_input(c, fd)
368
+ render()
369
+ finally:
370
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
371
+ sys.stdout.write('\033[?25h\033[2J\033[H')
372
+ sys.stdout.flush()
373
+
374
+ if ui.result:
375
+ print(ui.result)
376
+
377
+ context['output'] = ui.result or 'Build cancelled.'
378
+ context['messages'] = context.get('messages', [])