npcsh 1.1.16__py3-none-any.whl → 1.1.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. npcsh/_state.py +138 -100
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +28 -0
  4. npcsh/benchmark/npcsh_agent.py +296 -0
  5. npcsh/benchmark/runner.py +611 -0
  6. npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
  7. npcsh/build.py +2 -4
  8. npcsh/completion.py +2 -6
  9. npcsh/config.py +1 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/execution.py +0 -1
  13. npcsh/guac.py +0 -1
  14. npcsh/mcp_helpers.py +2 -3
  15. npcsh/mcp_server.py +5 -10
  16. npcsh/npc.py +10 -11
  17. npcsh/npc_team/jinxs/bin/benchmark.jinx +146 -0
  18. npcsh/npc_team/jinxs/bin/nql.jinx +7 -7
  19. npcsh/npc_team/jinxs/bin/roll.jinx +20 -23
  20. npcsh/npc_team/jinxs/bin/sample.jinx +6 -7
  21. npcsh/npc_team/jinxs/bin/sync.jinx +6 -6
  22. npcsh/npc_team/jinxs/bin/vixynt.jinx +8 -8
  23. npcsh/npc_team/jinxs/incognide/add_tab.jinx +11 -0
  24. npcsh/npc_team/jinxs/incognide/close_pane.jinx +9 -0
  25. npcsh/npc_team/jinxs/incognide/close_tab.jinx +10 -0
  26. npcsh/npc_team/jinxs/incognide/confirm.jinx +10 -0
  27. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +9 -0
  28. npcsh/npc_team/jinxs/{npc_studio/npc-studio.jinx → incognide/incognide.jinx} +2 -2
  29. npcsh/npc_team/jinxs/incognide/list_panes.jinx +8 -0
  30. npcsh/npc_team/jinxs/incognide/navigate.jinx +10 -0
  31. npcsh/npc_team/jinxs/incognide/notify.jinx +10 -0
  32. npcsh/npc_team/jinxs/incognide/open_pane.jinx +13 -0
  33. npcsh/npc_team/jinxs/incognide/read_pane.jinx +9 -0
  34. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +10 -0
  35. npcsh/npc_team/jinxs/incognide/send_message.jinx +10 -0
  36. npcsh/npc_team/jinxs/incognide/split_pane.jinx +12 -0
  37. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +10 -0
  38. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +10 -0
  39. npcsh/npc_team/jinxs/incognide/write_file.jinx +11 -0
  40. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +9 -0
  41. npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +4 -4
  42. npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +1 -1
  43. npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +2 -2
  44. npcsh/npc_team/jinxs/lib/computer_use/click.jinx +2 -2
  45. npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +1 -1
  46. npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +1 -1
  47. npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +1 -1
  48. npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +2 -2
  49. npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +1 -1
  50. npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +1 -1
  51. npcsh/npc_team/jinxs/lib/core/chat.jinx +4 -4
  52. npcsh/npc_team/jinxs/lib/core/cmd.jinx +4 -4
  53. npcsh/npc_team/jinxs/lib/core/compress.jinx +8 -8
  54. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +3 -0
  55. npcsh/npc_team/jinxs/lib/core/ots.jinx +7 -7
  56. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +348 -0
  57. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +339 -0
  58. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +418 -0
  59. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  60. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +388 -0
  61. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +283 -0
  62. npcsh/npc_team/jinxs/lib/core/search.jinx +52 -129
  63. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  64. npcsh/npc_team/jinxs/lib/core/sleep.jinx +29 -18
  65. npcsh/npc_team/jinxs/lib/core/sql.jinx +15 -11
  66. npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +7 -7
  67. npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +8 -9
  68. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +389 -78
  69. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +373 -56
  70. npcsh/npc_team/jinxs/lib/utils/build.jinx +5 -5
  71. npcsh/npc_team/jinxs/lib/utils/compile.jinx +2 -2
  72. npcsh/npc_team/jinxs/lib/utils/help.jinx +1 -1
  73. npcsh/npc_team/jinxs/lib/utils/init.jinx +5 -5
  74. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +300 -145
  75. npcsh/npc_team/jinxs/lib/utils/serve.jinx +2 -2
  76. npcsh/npc_team/jinxs/lib/utils/set.jinx +2 -2
  77. npcsh/npc_team/jinxs/lib/utils/switch.jinx +3 -3
  78. npcsh/npc_team/jinxs/lib/utils/switches.jinx +1 -1
  79. npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +2 -2
  80. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  81. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  82. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  83. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  84. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  85. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  86. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  87. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  88. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  89. {npcsh-1.1.16.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/yap.jinx +8 -2
  90. npcsh/npc_team/sibiji.npc +1 -1
  91. npcsh/npcsh.py +87 -46
  92. npcsh/plonk.py +0 -1
  93. npcsh/pti.py +0 -1
  94. npcsh/routes.py +1 -3
  95. npcsh/spool.py +0 -1
  96. npcsh/ui.py +0 -1
  97. npcsh/wander.py +0 -1
  98. npcsh/yap.py +0 -1
  99. npcsh-1.1.18.data/data/npcsh/npc_team/add_tab.jinx +11 -0
  100. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  101. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  102. npcsh-1.1.18.data/data/npcsh/npc_team/benchmark.jinx +146 -0
  103. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +4 -4
  104. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +1 -1
  105. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +5 -5
  106. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +4 -4
  107. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +2 -2
  108. npcsh-1.1.18.data/data/npcsh/npc_team/close_pane.jinx +9 -0
  109. npcsh-1.1.18.data/data/npcsh/npc_team/close_tab.jinx +10 -0
  110. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +4 -4
  111. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +2 -2
  112. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +8 -8
  113. npcsh-1.1.18.data/data/npcsh/npc_team/confirm.jinx +10 -0
  114. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +7 -7
  115. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  116. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  117. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +8 -9
  118. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +3 -0
  119. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  120. npcsh-1.1.18.data/data/npcsh/npc_team/focus_pane.jinx +9 -0
  121. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  122. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +1 -1
  123. npcsh-1.1.16.data/data/npcsh/npc_team/npc-studio.jinx → npcsh-1.1.18.data/data/npcsh/npc_team/incognide.jinx +2 -2
  124. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +5 -5
  125. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  126. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +1 -1
  127. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  128. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +1 -1
  129. npcsh-1.1.18.data/data/npcsh/npc_team/list_panes.jinx +8 -0
  130. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  131. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  132. npcsh-1.1.18.data/data/npcsh/npc_team/navigate.jinx +10 -0
  133. npcsh-1.1.18.data/data/npcsh/npc_team/notify.jinx +10 -0
  134. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +7 -7
  135. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +2 -2
  136. npcsh-1.1.18.data/data/npcsh/npc_team/open_pane.jinx +13 -0
  137. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +7 -7
  138. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  139. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  140. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  141. npcsh-1.1.18.data/data/npcsh/npc_team/read_pane.jinx +9 -0
  142. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  143. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +20 -23
  144. npcsh-1.1.18.data/data/npcsh/npc_team/run_terminal.jinx +10 -0
  145. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +6 -7
  146. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +1 -1
  147. npcsh-1.1.18.data/data/npcsh/npc_team/search.jinx +54 -0
  148. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  149. npcsh-1.1.18.data/data/npcsh/npc_team/send_message.jinx +10 -0
  150. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +2 -2
  151. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +2 -2
  152. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +1 -1
  153. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +1 -1
  154. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +29 -18
  155. npcsh-1.1.18.data/data/npcsh/npc_team/split_pane.jinx +12 -0
  156. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  157. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  158. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +3 -3
  159. npcsh-1.1.18.data/data/npcsh/npc_team/switch_npc.jinx +10 -0
  160. npcsh-1.1.18.data/data/npcsh/npc_team/switch_tab.jinx +10 -0
  161. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +1 -1
  162. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +6 -6
  163. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +2 -2
  164. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +2 -2
  165. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +1 -1
  166. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +8 -8
  167. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +1 -1
  168. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  169. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  170. npcsh-1.1.18.data/data/npcsh/npc_team/write_file.jinx +11 -0
  171. {npcsh/npc_team/jinxs/bin → npcsh-1.1.18.data/data/npcsh/npc_team}/yap.jinx +8 -2
  172. npcsh-1.1.18.data/data/npcsh/npc_team/zen_mode.jinx +9 -0
  173. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/METADATA +99 -7
  174. npcsh-1.1.18.dist-info/RECORD +235 -0
  175. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  176. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +2 -3
  177. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  178. npcsh/npc_team/jinxs/bin/wander.jinx +0 -152
  179. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  180. npcsh-1.1.16.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  181. npcsh-1.1.16.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  182. npcsh-1.1.16.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  183. npcsh-1.1.16.data/data/npcsh/npc_team/search.jinx +0 -131
  184. npcsh-1.1.16.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  185. npcsh-1.1.16.data/data/npcsh/npc_team/spool.jinx +0 -161
  186. npcsh-1.1.16.data/data/npcsh/npc_team/sql.jinx +0 -16
  187. npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +0 -152
  188. npcsh-1.1.16.dist-info/RECORD +0 -170
  189. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  190. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  191. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  192. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  193. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  194. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  195. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  196. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  197. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  198. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  199. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  200. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  201. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  202. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  203. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  204. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  205. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  206. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  207. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  208. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  209. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  210. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  211. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  212. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  213. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  214. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  215. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  216. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  217. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,544 @@
1
+ jinx_name: guac
2
+ description: Interactive Python data analysis TUI - live variable inspector, DataFrame viewer, code execution
3
+ inputs:
4
+ - model: null
5
+ - provider: null
6
+ - plots_dir: null
7
+
8
+ steps:
9
+ - name: guac_tui
10
+ engine: python
11
+ code: |
12
+ import os
13
+ import sys
14
+ import io
15
+ import re
16
+ import tty
17
+ import termios
18
+ import traceback
19
+ import textwrap
20
+ import select
21
+ from pathlib import Path
22
+ from datetime import datetime
23
+ from termcolor import colored
24
+
25
+ import numpy as np
26
+ import pandas as pd
27
+ import matplotlib
28
+ matplotlib.use('Agg') # Non-interactive backend for TUI
29
+ import matplotlib.pyplot as plt
30
+
31
+ from npcpy.llm_funcs import get_llm_response
32
+ from npcpy.npc_sysenv import render_markdown, get_system_message
33
+
34
+ npc = context.get('npc')
35
+ team = context.get('team')
36
+ messages = context.get('messages', [])
37
+ plots_dir = context.get('plots_dir') or os.path.expanduser("~/.npcsh/plots")
38
+
39
+ # Resolve npc if it's a string (npc name) rather than NPC object
40
+ if isinstance(npc, str) and team:
41
+ npc = team.get(npc) if hasattr(team, 'get') else None
42
+ elif isinstance(npc, str):
43
+ npc = None
44
+
45
+ model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
46
+ provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
47
+
48
+ os.makedirs(plots_dir, exist_ok=True)
49
+
50
+ # ========== State ==========
51
+ class GuacState:
52
+ def __init__(self):
53
+ self.locals = {
54
+ 'np': np, 'pd': pd, 'plt': plt,
55
+ 'Path': Path, 'os': os
56
+ }
57
+ self.history = [] # Code history
58
+ self.output_lines = [] # Current output
59
+ self.plots = [] # Saved plot paths
60
+ self.current_input = ""
61
+ self.cursor_pos = 0
62
+ self.history_idx = -1
63
+ self.scroll_offset = 0
64
+ self.panel = 0 # 0=output, 1=variables, 2=dataframes, 3=plots
65
+ self.selected_var = 0
66
+ self.var_scroll = 0
67
+ self.status = "Ready"
68
+ self.mode = "code" # code, natural, inspect
69
+ self.inspecting = None # Variable being inspected
70
+ self.df_row_offset = 0
71
+ self.df_col_offset = 0
72
+
73
+ state = GuacState()
74
+
75
+ # ========== Helpers ==========
76
+ def get_size():
77
+ try:
78
+ s = os.get_terminal_size()
79
+ return s.columns, s.lines
80
+ except:
81
+ return 80, 24
82
+
83
+ def wrap_text(text, width):
84
+ lines = []
85
+ for line in str(text).split('\n'):
86
+ if len(line) <= width:
87
+ lines.append(line)
88
+ else:
89
+ lines.extend(textwrap.wrap(line, width) or [''])
90
+ return lines
91
+
92
+ def get_user_vars():
93
+ """Get user-defined variables (not builtins)"""
94
+ skip = {'np', 'pd', 'plt', 'Path', 'os', '__builtins__'}
95
+ return {k: v for k, v in state.locals.items()
96
+ if not k.startswith('_') and k not in skip}
97
+
98
+ def var_info(name, value):
99
+ """Get info string for a variable"""
100
+ if isinstance(value, pd.DataFrame):
101
+ return f"DataFrame {value.shape}"
102
+ elif isinstance(value, pd.Series):
103
+ return f"Series ({len(value)})"
104
+ elif isinstance(value, np.ndarray):
105
+ return f"ndarray {value.shape} {value.dtype}"
106
+ elif isinstance(value, (list, tuple)):
107
+ return f"{type(value).__name__} ({len(value)})"
108
+ elif isinstance(value, dict):
109
+ return f"dict ({len(value)} keys)"
110
+ elif isinstance(value, str):
111
+ return f"str ({len(value)} chars)"
112
+ elif isinstance(value, (int, float)):
113
+ return f"{type(value).__name__}: {value}"
114
+ else:
115
+ return type(value).__name__
116
+
117
+ def execute_code(code):
118
+ """Execute Python code and capture output"""
119
+ output = io.StringIO()
120
+ old_stdout, old_stderr = sys.stdout, sys.stderr
121
+
122
+ try:
123
+ sys.stdout = output
124
+ sys.stderr = output
125
+
126
+ # Try as expression first
127
+ if '\n' not in code.strip():
128
+ try:
129
+ result = eval(compile(code, "<input>", "eval"), state.locals)
130
+ if result is not None:
131
+ print(repr(result))
132
+ return output.getvalue(), None
133
+ except SyntaxError:
134
+ pass
135
+
136
+ # Execute as statements
137
+ exec(compile(code, "<input>", "exec"), state.locals)
138
+ return output.getvalue(), None
139
+
140
+ except Exception as e:
141
+ return output.getvalue(), traceback.format_exc()
142
+ finally:
143
+ sys.stdout, sys.stderr = old_stdout, old_stderr
144
+
145
+ def save_plot():
146
+ """Save current matplotlib figure"""
147
+ if plt.get_fignums():
148
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
149
+ path = os.path.join(plots_dir, f"plot_{timestamp}.png")
150
+ plt.savefig(path, dpi=150, bbox_inches='tight')
151
+ plt.close()
152
+ state.plots.append(path)
153
+ return path
154
+ return None
155
+
156
+ def load_file(path):
157
+ """Auto-load file based on extension"""
158
+ path = Path(path).expanduser()
159
+ if not path.exists():
160
+ return None, f"File not found: {path}"
161
+
162
+ ext = path.suffix.lower()
163
+ var_name = path.stem.replace(' ', '_').replace('-', '_')[:20]
164
+
165
+ try:
166
+ if ext == '.csv':
167
+ df = pd.read_csv(path)
168
+ state.locals[var_name] = df
169
+ return var_name, f"Loaded CSV as '{var_name}': {df.shape}"
170
+ elif ext in ['.xlsx', '.xls']:
171
+ df = pd.read_excel(path)
172
+ state.locals[var_name] = df
173
+ return var_name, f"Loaded Excel as '{var_name}': {df.shape}"
174
+ elif ext == '.json':
175
+ import json
176
+ with open(path) as f:
177
+ data = json.load(f)
178
+ state.locals[var_name] = data
179
+ return var_name, f"Loaded JSON as '{var_name}'"
180
+ elif ext == '.npy':
181
+ arr = np.load(path)
182
+ state.locals[var_name] = arr
183
+ return var_name, f"Loaded numpy as '{var_name}': {arr.shape}"
184
+ elif ext in ['.txt', '.md']:
185
+ with open(path) as f:
186
+ text = f.read()
187
+ state.locals[var_name] = text
188
+ return var_name, f"Loaded text as '{var_name}': {len(text)} chars"
189
+ else:
190
+ with open(path, 'rb') as f:
191
+ data = f.read()
192
+ state.locals[var_name] = data
193
+ return var_name, f"Loaded binary as '{var_name}': {len(data)} bytes"
194
+ except Exception as e:
195
+ return None, f"Error: {e}"
196
+
197
+ # ========== Rendering ==========
198
+ def render_screen():
199
+ width, height = get_size()
200
+ out = []
201
+
202
+ out.append("\033[2J\033[H")
203
+
204
+ # ===== HEADER =====
205
+ mode_colors = {"code": "\033[32m", "natural": "\033[35m", "inspect": "\033[33m"}
206
+ mode_str = f"{mode_colors[state.mode]}[{state.mode}]\033[0m"
207
+ header = f" GUAC - Python Data Analysis {mode_str} "
208
+ status_color = "\033[33m" if "..." in state.status else "\033[32m"
209
+ out.append(f"\033[1;1H\033[42;30;1m{header.ljust(width)}\033[0m")
210
+ out.append(f"\033[1;{width-len(state.status)-3}H{status_color}[{state.status}]\033[0m")
211
+
212
+ # ===== LAYOUT =====
213
+ # Left: Output (80%)
214
+ # Right: Variables/DataFrames/Plots (20%)
215
+ left_w = int(width * 0.8)
216
+ right_w = width - left_w
217
+ panel_h = height - 6 # Leave room for input
218
+
219
+ user_vars = get_user_vars()
220
+ var_names = list(user_vars.keys())
221
+
222
+ # ===== LEFT PANEL: Output =====
223
+ out.append(f"\033[3;1H\033[32m Output \033[90m{'─' * (left_w-9)}┬\033[0m")
224
+ for i in range(panel_h - 1):
225
+ out.append(f"\033[{4+i};{left_w}H\033[90m│\033[0m")
226
+ out.append(f"\033[{3+panel_h};1H\033[90m{'─' * (left_w-1)}┴\033[0m")
227
+
228
+ output_w = left_w - 3
229
+ output_h = panel_h - 2
230
+
231
+ if state.inspecting and state.inspecting in user_vars:
232
+ # Inspection mode - show in output panel
233
+ val = user_vars[state.inspecting]
234
+ out.append(f"\033[4;2H\033[33mInspecting: {state.inspecting}\033[0m")
235
+
236
+ if isinstance(val, pd.DataFrame):
237
+ cols = list(val.columns)[state.df_col_offset:state.df_col_offset + 5]
238
+ col_w = (output_w - 6) // max(len(cols), 1)
239
+ header = " " + "".join(f"{str(c)[:col_w]:<{col_w}}" for c in cols)
240
+ out.append(f"\033[5;2H\033[1m{header[:output_w]}\033[0m")
241
+ for i, (idx, row) in enumerate(val.iloc[state.df_row_offset:state.df_row_offset + output_h - 3].iterrows()):
242
+ row_str = f"{idx:<4} " + "".join(f"{str(row[c])[:col_w]:<{col_w}}" for c in cols)
243
+ out.append(f"\033[{6+i};2H{row_str[:output_w]}")
244
+ out.append(f"\033[{3+panel_h-1};2H\033[90mArrows:scroll ESC:exit inspect\033[0m")
245
+ elif isinstance(val, np.ndarray):
246
+ out.append(f"\033[5;2H\033[90mShape: {val.shape} dtype: {val.dtype}\033[0m")
247
+ for i, line in enumerate(wrap_text(repr(val), output_w)[:output_h-2]):
248
+ out.append(f"\033[{6+i};2H{line}")
249
+ else:
250
+ for i, line in enumerate(wrap_text(repr(val), output_w)[:output_h]):
251
+ out.append(f"\033[{5+i};2H{line}")
252
+ else:
253
+ # Normal output
254
+ visible = state.output_lines[state.scroll_offset:state.scroll_offset + output_h]
255
+ for i, line in enumerate(visible):
256
+ if 'Error' in line or 'Traceback' in line:
257
+ out.append(f"\033[{4+i};2H\033[31m{line[:output_w]}\033[0m")
258
+ elif line.startswith('>>>'):
259
+ out.append(f"\033[{4+i};2H\033[32m{line[:output_w]}\033[0m")
260
+ else:
261
+ out.append(f"\033[{4+i};2H{line[:output_w]}")
262
+
263
+ if not state.output_lines:
264
+ hints = [
265
+ "\033[90mWelcome to GUAC!\033[0m",
266
+ "", "Type Python code and press Enter",
267
+ "Drop file paths to auto-load", "",
268
+ "\033[33mKeys:\033[0m Tab:panels Arrows:nav",
269
+ "Ctrl+N:NL Ctrl+S:save Ctrl+Q:quit",
270
+ ]
271
+ for i, hint in enumerate(hints[:output_h]):
272
+ out.append(f"\033[{4+i};2H{hint}")
273
+
274
+ # ===== RIGHT PANEL: Variables/Plots =====
275
+ right_x = left_w + 1
276
+ panel_names = ["Vars", "DFs", "Plots"]
277
+ tabs = ""
278
+ for i, name in enumerate(panel_names):
279
+ if i == state.panel:
280
+ tabs += f"\033[47;30m {name} \033[0m"
281
+ else:
282
+ tabs += f"\033[90m {name} \033[0m"
283
+ out.append(f"\033[3;{right_x}H{tabs}")
284
+
285
+ # Right panel content
286
+ rpanel_w = right_w - 2
287
+ rpanel_h = panel_h - 2
288
+
289
+ if state.panel == 0: # Variables
290
+ for i, name in enumerate(var_names[state.var_scroll:state.var_scroll + rpanel_h]):
291
+ idx = i + state.var_scroll
292
+ value = user_vars[name]
293
+ info = var_info(name, value)
294
+ display = f"{name[:10]:<10} {info[:rpanel_w-12]}"
295
+ if idx == state.selected_var:
296
+ out.append(f"\033[{4+i};{right_x}H\033[47;30m>{display[:rpanel_w]}\033[0m")
297
+ elif isinstance(value, pd.DataFrame):
298
+ out.append(f"\033[{4+i};{right_x}H\033[34m {display[:rpanel_w]}\033[0m")
299
+ elif isinstance(value, np.ndarray):
300
+ out.append(f"\033[{4+i};{right_x}H\033[35m {display[:rpanel_w]}\033[0m")
301
+ else:
302
+ out.append(f"\033[{4+i};{right_x}H {display[:rpanel_w]}")
303
+ if not var_names:
304
+ out.append(f"\033[5;{right_x}H\033[90mNo vars\033[0m")
305
+
306
+ elif state.panel == 1: # DataFrames
307
+ dfs = {k: v for k, v in user_vars.items() if isinstance(v, pd.DataFrame)}
308
+ df_names = list(dfs.keys())
309
+ for i, name in enumerate(df_names[:rpanel_h]):
310
+ df = dfs[name]
311
+ info = f"{df.shape[0]}x{df.shape[1]}"
312
+ out.append(f"\033[{4+i};{right_x}H\033[34m {name[:10]:<10} {info}\033[0m")
313
+ if not df_names:
314
+ out.append(f"\033[5;{right_x}H\033[90mNo DFs\033[0m")
315
+
316
+ elif state.panel == 2: # Plots
317
+ if state.plots:
318
+ for i, path in enumerate(state.plots[-(rpanel_h):]):
319
+ name = os.path.basename(path)[:rpanel_w]
320
+ out.append(f"\033[{4+i};{right_x}H {name}")
321
+ else:
322
+ out.append(f"\033[5;{right_x}H\033[90mNo plots\033[0m")
323
+ out.append(f"\033[6;{right_x}H\033[90mCtrl+S save\033[0m")
324
+
325
+ # ===== INPUT LINE =====
326
+ input_y = height - 2
327
+ visible_prompt = "NL> " if state.mode == "natural" else ">>> "
328
+ out.append(f"\033[{input_y};1H\033[90m{'─' * width}\033[0m")
329
+ # Clear the input line first, then write prompt and input
330
+ out.append(f"\033[{height-1};1H\033[K") # Clear line
331
+ out.append(f"\033[{height-1};1H\033[32m{visible_prompt}\033[0m{state.current_input}")
332
+
333
+ # Position cursor after prompt + cursor_pos characters
334
+ cursor_col = len(visible_prompt) + state.cursor_pos + 1 # +1 for 1-indexed columns
335
+ out.append(f"\033[{height-1};{cursor_col}H")
336
+
337
+ # ===== FOOTER =====
338
+ hints = "Tab:panels Arrows:nav Ctrl+N:NL Ctrl+S:save Ctrl+D:del Ctrl+Q:quit"
339
+ out.append(f"\033[{height};1H\033[90m{hints[:width]}\033[0m")
340
+
341
+ sys.stdout.write(''.join(out))
342
+ sys.stdout.flush()
343
+
344
+ # ========== Input Handling ==========
345
+ def handle_input(c):
346
+ if c == '\x11': # Ctrl+Q - quit
347
+ return False
348
+
349
+ elif c == '\t': # Tab - cycle panels
350
+ state.panel = (state.panel + 1) % 3
351
+ state.selected_var = 0
352
+ state.var_scroll = 0
353
+
354
+ elif c == '\x0e': # Ctrl+N - toggle natural language
355
+ state.mode = "natural" if state.mode == "code" else "code"
356
+ state.status = f"{state.mode} mode"
357
+
358
+ elif c == '\x13': # Ctrl+S - save plot
359
+ path = save_plot()
360
+ if path:
361
+ state.output_lines.append(f"Plot saved: {path}")
362
+ state.status = "Plot saved"
363
+ else:
364
+ state.status = "No plot to save"
365
+
366
+ elif c == '\x04': # Ctrl+D - delete variable
367
+ user_vars = get_user_vars()
368
+ var_names = list(user_vars.keys())
369
+ if var_names and state.selected_var < len(var_names):
370
+ name = var_names[state.selected_var]
371
+ del state.locals[name]
372
+ state.output_lines.append(f"Deleted: {name}")
373
+ state.selected_var = max(0, state.selected_var - 1)
374
+
375
+ elif c == '\x1b': # Escape sequences
376
+ # Check if more input is available (escape sequence) or standalone ESC
377
+ if select.select([sys.stdin], [], [], 0.05)[0]:
378
+ c2 = sys.stdin.read(1)
379
+ if c2 == '[':
380
+ c3 = sys.stdin.read(1)
381
+ if c3 == 'A': # Up
382
+ if state.current_input == "" and state.history:
383
+ state.history_idx = max(0, state.history_idx - 1) if state.history_idx >= 0 else len(state.history) - 1
384
+ state.current_input = state.history[state.history_idx]
385
+ state.cursor_pos = len(state.current_input)
386
+ elif not state.current_input:
387
+ if state.inspecting:
388
+ state.df_row_offset = max(0, state.df_row_offset - 1)
389
+ elif state.panel > 0:
390
+ state.selected_var = max(0, state.selected_var - 1)
391
+ else:
392
+ state.scroll_offset = max(0, state.scroll_offset - 1)
393
+ elif c3 == 'B': # Down
394
+ if state.current_input == "" and state.history and state.history_idx >= 0:
395
+ state.history_idx = min(len(state.history) - 1, state.history_idx + 1)
396
+ state.current_input = state.history[state.history_idx]
397
+ state.cursor_pos = len(state.current_input)
398
+ elif not state.current_input:
399
+ if state.inspecting:
400
+ state.df_row_offset += 1
401
+ elif state.panel > 0:
402
+ user_vars = get_user_vars()
403
+ state.selected_var = min(state.selected_var + 1, len(user_vars) - 1)
404
+ else:
405
+ state.scroll_offset += 1
406
+ elif c3 == 'C': # Right
407
+ state.cursor_pos = min(len(state.current_input), state.cursor_pos + 1)
408
+ elif c3 == 'D': # Left
409
+ state.cursor_pos = max(0, state.cursor_pos - 1)
410
+ else:
411
+ # Standalone ESC - exit inspect mode or clear input
412
+ if state.inspecting:
413
+ state.inspecting = None
414
+ state.mode = "code"
415
+ elif state.current_input:
416
+ state.current_input = ""
417
+ state.cursor_pos = 0
418
+
419
+ elif c == '\r' or c == '\n': # Enter
420
+ if state.current_input.strip():
421
+ code = state.current_input.strip()
422
+
423
+ # Check for file path (drag & drop)
424
+ if os.path.exists(os.path.expanduser(code.strip("'\""))):
425
+ name, msg = load_file(code.strip("'\""))
426
+ state.output_lines.append(msg)
427
+ state.status = "File loaded" if name else "Load failed"
428
+
429
+ elif state.mode == "natural":
430
+ # Natural language -> code generation
431
+ state.status = "Generating code..."
432
+ render_screen()
433
+
434
+ var_context = "Variables: " + ", ".join(f"{k}({var_info(k,v)})" for k,v in get_user_vars().items())
435
+ prompt = f"{var_context}\n\nRequest: {code}\n\nGenerate Python code. Return ONLY code, no explanation."
436
+
437
+ resp = get_llm_response(prompt, model=model, provider=provider, npc=npc)
438
+ gen_code = str(resp.get('response', ''))
439
+
440
+ # Extract code from markdown
441
+ if '```python' in gen_code:
442
+ gen_code = gen_code.split('```python')[1].split('```')[0]
443
+ elif '```' in gen_code:
444
+ gen_code = gen_code.split('```')[1].split('```')[0]
445
+ gen_code = gen_code.strip()
446
+
447
+ state.output_lines.append(f">>> # Generated from: {code[:40]}...")
448
+ state.output_lines.append(gen_code)
449
+ state.output_lines.append("Execute? (y to run)")
450
+ state.history.append(code)
451
+ state.status = "Confirm execution"
452
+ # Store for potential execution
453
+ state.locals['__pending_code__'] = gen_code
454
+
455
+ elif code == 'y' and '__pending_code__' in state.locals:
456
+ # Execute pending generated code
457
+ gen_code = state.locals.pop('__pending_code__')
458
+ output, error = execute_code(gen_code)
459
+ if output:
460
+ state.output_lines.extend(output.split('\n'))
461
+ if error:
462
+ state.output_lines.extend(error.split('\n'))
463
+ state.status = "Executed"
464
+
465
+ else:
466
+ # Direct code execution
467
+ state.output_lines.append(f">>> {code}")
468
+ state.history.append(code)
469
+
470
+ output, error = execute_code(code)
471
+ if output:
472
+ state.output_lines.extend(output.split('\n'))
473
+ if error:
474
+ state.output_lines.extend(error.split('\n'))
475
+ state.status = "Error"
476
+ else:
477
+ state.status = "OK"
478
+
479
+ state.current_input = ""
480
+ state.cursor_pos = 0
481
+ state.history_idx = -1
482
+ state.scroll_offset = max(0, len(state.output_lines) - 10)
483
+
484
+ elif c == '\x7f' or c == '\x08': # Backspace
485
+ if state.cursor_pos > 0:
486
+ state.current_input = state.current_input[:state.cursor_pos-1] + state.current_input[state.cursor_pos:]
487
+ state.cursor_pos -= 1
488
+
489
+ elif c == '\x15': # Ctrl+U - clear line
490
+ state.current_input = ""
491
+ state.cursor_pos = 0
492
+
493
+ elif c == '\x01': # Ctrl+A - start of line
494
+ state.cursor_pos = 0
495
+
496
+ elif c == '\x05': # Ctrl+E - end of line
497
+ state.cursor_pos = len(state.current_input)
498
+
499
+ elif c == '\x0c': # Ctrl+L - clear output
500
+ state.output_lines = []
501
+ state.scroll_offset = 0
502
+
503
+ elif c >= ' ' and c <= '~': # Printable
504
+ state.current_input = state.current_input[:state.cursor_pos] + c + state.current_input[state.cursor_pos:]
505
+ state.cursor_pos += 1
506
+
507
+ return True
508
+
509
+ # ========== Main Loop ==========
510
+ fd = sys.stdin.fileno()
511
+ old_settings = termios.tcgetattr(fd)
512
+
513
+ try:
514
+ tty.setcbreak(fd)
515
+ sys.stdout.write('\033[?25h') # Show cursor
516
+
517
+ render_screen()
518
+
519
+ while True:
520
+ c = sys.stdin.read(1)
521
+ if not handle_input(c):
522
+ break
523
+ render_screen()
524
+
525
+ finally:
526
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
527
+ sys.stdout.write('\033[2J\033[H')
528
+ sys.stdout.flush()
529
+
530
+ # Final summary
531
+ user_vars = get_user_vars()
532
+ if user_vars:
533
+ print(colored("=== GUAC SESSION ===\n", "green"))
534
+ print("Variables:")
535
+ for name, val in user_vars.items():
536
+ print(f" {name}: {var_info(name, val)}")
537
+ if state.plots:
538
+ print(f"\nPlots saved: {len(state.plots)}")
539
+ for p in state.plots[-5:]:
540
+ print(f" {p}")
541
+
542
+ context['output'] = "Exited guac mode."
543
+ context['messages'] = messages
544
+ context['guac_locals'] = state.locals
@@ -1,7 +1,7 @@
1
1
  jinx_name: help
2
2
  description: Show help for commands, NPCs, or Jinxs
3
3
  inputs:
4
- - topic: null
4
+ - topic: null
5
5
  steps:
6
6
  - name: show_help
7
7
  engine: python
@@ -1,7 +1,7 @@
1
- jinx_name: "npc-studio"
1
+ jinx_name: "incognide"
2
2
  description: "Start npc studio"
3
3
  inputs:
4
- - user_command: ""
4
+ - user_command: ""
5
5
  steps:
6
6
  - name: "launch_npc_studio"
7
7
  engine: "python"
@@ -1,11 +1,11 @@
1
1
  jinx_name: "init"
2
2
  description: "Initialize NPC project"
3
3
  inputs:
4
- - directory: "." # The directory where the NPC project should be initialized.
5
- - templates: "" # Optional templates to use for initialization.
6
- - context: "" # Optional context for project initialization.
7
- - model: "" # Optional LLM model to set as default for the project.
8
- - provider: "" # Optional LLM provider to set as default for the project.
4
+ - directory: "."
5
+ - templates: ""
6
+ - context: ""
7
+ - model: ""
8
+ - provider: ""
9
9
  steps:
10
10
  - name: "initialize_project"
11
11
  engine: "python"