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