npcsh 1.1.19__py3-none-any.whl → 1.1.21__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 (173) hide show
  1. npcsh/_state.py +16 -78
  2. npcsh/diff_viewer.py +3 -3
  3. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  4. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +18 -7
  5. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +18 -7
  6. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +20 -9
  7. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +53 -15
  8. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  9. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +393 -317
  10. npcsh/npc_team/jinxs/lib/utils/models.jinx +343 -0
  11. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +8 -7
  12. npcsh/npc_team/jinxs/modes/alicanto.jinx +1573 -296
  13. npcsh/npc_team/jinxs/modes/arxiv.jinx +6 -6
  14. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  15. npcsh/npc_team/jinxs/modes/corca.jinx +4 -4
  16. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  17. npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
  18. npcsh/npc_team/jinxs/modes/kg.jinx +941 -0
  19. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  20. npcsh/npc_team/jinxs/modes/nql.jinx +460 -0
  21. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  22. npcsh/npc_team/jinxs/modes/plonk.jinx +490 -304
  23. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  24. npcsh/npc_team/jinxs/modes/reattach.jinx +4 -4
  25. npcsh/npc_team/jinxs/modes/spool.jinx +4 -4
  26. npcsh/npc_team/jinxs/modes/team.jinx +504 -0
  27. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  28. npcsh/npc_team/jinxs/modes/wander.jinx +455 -182
  29. npcsh/npc_team/jinxs/modes/yap.jinx +10 -3
  30. npcsh/npcsh.py +112 -47
  31. npcsh/routes.py +12 -3
  32. npcsh/salmon_simulation.py +0 -0
  33. npcsh-1.1.21.data/data/npcsh/npc_team/alicanto.jinx +1633 -0
  34. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/arxiv.jinx +6 -6
  35. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  36. npcsh-1.1.21.data/data/npcsh/npc_team/compress.jinx +428 -0
  37. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  38. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.jinx +4 -4
  39. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/db_search.jinx +18 -7
  40. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/file_search.jinx +18 -7
  41. npcsh-1.1.21.data/data/npcsh/npc_team/git.jinx +795 -0
  42. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.jinx +4 -4
  43. npcsh-1.1.21.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  44. npcsh-1.1.21.data/data/npcsh/npc_team/kg.jinx +941 -0
  45. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kg_search.jinx +20 -9
  46. npcsh-1.1.21.data/data/npcsh/npc_team/memories.jinx +414 -0
  47. npcsh-1.1.21.data/data/npcsh/npc_team/models.jinx +343 -0
  48. npcsh-1.1.21.data/data/npcsh/npc_team/nql.jinx +460 -0
  49. npcsh-1.1.21.data/data/npcsh/npc_team/papers.jinx +578 -0
  50. npcsh-1.1.21.data/data/npcsh/npc_team/plonk.jinx +565 -0
  51. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/pti.jinx +1 -1
  52. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/reattach.jinx +4 -4
  53. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/setup.jinx +8 -7
  54. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.jinx +4 -4
  55. npcsh-1.1.21.data/data/npcsh/npc_team/team.jinx +504 -0
  56. npcsh-1.1.21.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  57. npcsh-1.1.21.data/data/npcsh/npc_team/wander.jinx +728 -0
  58. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/web_search.jinx +53 -15
  59. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.jinx +10 -3
  60. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/METADATA +2 -2
  61. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/RECORD +147 -148
  62. npcsh-1.1.21.dist-info/entry_points.txt +11 -0
  63. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -299
  64. npcsh/npc_team/jinxs/bin/memories.jinx +0 -316
  65. npcsh/npc_team/jinxs/bin/nql.jinx +0 -141
  66. npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
  67. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  68. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  69. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  70. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  71. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  72. npcsh/npc_team/plonkjr.npc +0 -23
  73. npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  74. npcsh-1.1.19.data/data/npcsh/npc_team/compress.jinx +0 -140
  75. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +0 -299
  76. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  77. npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  78. npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  79. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +0 -316
  80. npcsh-1.1.19.data/data/npcsh/npc_team/nql.jinx +0 -141
  81. npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  82. npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +0 -379
  83. npcsh-1.1.19.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  84. npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  85. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +0 -327
  86. npcsh-1.1.19.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  87. npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +0 -455
  88. npcsh-1.1.19.dist-info/entry_points.txt +0 -22
  89. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  90. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  91. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  92. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  93. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  94. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  95. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  96. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.png +0 -0
  97. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  98. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  99. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/build.jinx +0 -0
  100. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/chat.jinx +0 -0
  101. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/click.jinx +0 -0
  102. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  103. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  104. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  105. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  106. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/compile.jinx +0 -0
  107. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  108. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/convene.jinx +0 -0
  109. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.npc +0 -0
  110. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.png +0 -0
  111. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca_example.png +0 -0
  112. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  113. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  114. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  115. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic.npc +0 -0
  116. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic4.png +0 -0
  117. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.npc +0 -0
  118. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.png +0 -0
  119. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/help.jinx +0 -0
  120. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  121. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/init.jinx +0 -0
  122. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  123. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  124. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  125. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  126. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  127. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  128. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  129. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/notify.jinx +0 -0
  130. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  131. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  132. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  133. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  134. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/ots.jinx +0 -0
  135. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/paste.jinx +0 -0
  136. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.npc +0 -0
  137. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.png +0 -0
  138. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  139. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/python.jinx +0 -0
  140. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  141. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/roll.jinx +0 -0
  142. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  143. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sample.jinx +0 -0
  144. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  145. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/search.jinx +0 -0
  146. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  147. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/serve.jinx +0 -0
  148. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/set.jinx +0 -0
  149. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sh.jinx +0 -0
  150. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/shh.jinx +0 -0
  151. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  152. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.png +0 -0
  153. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  154. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  155. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.png +0 -0
  156. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sql.jinx +0 -0
  157. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch.jinx +0 -0
  158. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  159. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  160. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switches.jinx +0 -0
  161. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sync.jinx +0 -0
  162. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  163. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  164. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  165. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/usage.jinx +0 -0
  166. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  167. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/wait.jinx +0 -0
  168. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  169. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.png +0 -0
  170. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  171. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/WHEEL +0 -0
  172. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/licenses/LICENSE +0 -0
  173. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,460 @@
1
+ jinx_name: nql
2
+ description: "Interactive DB viewer/manager or run NPC-SQL models"
3
+ interactive: true
4
+ inputs:
5
+ - models_dir: "~/.npcsh/npc_team/models"
6
+ - db: ""
7
+ - model: ""
8
+ - schema: ""
9
+ - show: ""
10
+ - cron: ""
11
+ - install_cron: ""
12
+
13
+ steps:
14
+ - name: run_nql
15
+ engine: python
16
+ code: |
17
+ import os
18
+ import sys
19
+ import tty
20
+ import termios
21
+ import select
22
+ from pathlib import Path
23
+
24
+ models_dir = context.get('models_dir') or '~/.npcsh/npc_team/models'
25
+ db_path = context.get('db') or ''
26
+ model_name = context.get('model') or ''
27
+ schema = context.get('schema') or ''
28
+ list_models = context.get('show') or ''
29
+ install_cron = context.get('install_cron') or ''
30
+
31
+ models_dir = os.path.expanduser(models_dir)
32
+
33
+ # Get db path from state if not specified
34
+ if not db_path:
35
+ from npcsh.config import NPCSH_DB_PATH
36
+ db_path = os.path.expanduser(NPCSH_DB_PATH)
37
+ else:
38
+ db_path = os.path.expanduser(db_path)
39
+
40
+ # Find NPC team directory
41
+ npc_dir = None
42
+ if state and state.team:
43
+ npc_dir = state.team.team_path
44
+ elif os.path.exists('./npc_team'):
45
+ npc_dir = './npc_team'
46
+ elif os.path.exists(os.path.expanduser('~/.npcsh/npc_team')):
47
+ npc_dir = os.path.expanduser('~/.npcsh/npc_team')
48
+
49
+ has_action = model_name or list_models or install_cron
50
+
51
+ # Interactive TUI mode when no args
52
+ if not has_action and sys.stdin.isatty():
53
+ # Stop spinner before taking over terminal
54
+ try:
55
+ from npcsh.ui import get_current_spinner
56
+ import time
57
+ spinner = get_current_spinner()
58
+ if spinner:
59
+ spinner._stop = True
60
+ if spinner._thread:
61
+ spinner._thread.join(timeout=0.5)
62
+ if spinner._key_thread:
63
+ spinner._key_thread.join(timeout=0.5)
64
+ sys.stdout.write('\r' + ' ' * 80 + '\r')
65
+ sys.stdout.flush()
66
+ time.sleep(0.1)
67
+ # Flush any pending input
68
+ while select.select([fd], [], [], 0)[0]:
69
+ os.read(fd, 1).decode('latin-1')
70
+ except:
71
+ pass
72
+
73
+ import sqlite3
74
+
75
+ class DBState:
76
+ def __init__(self):
77
+ self.mode = 'tables'
78
+ self.tables = []
79
+ self.selected_idx = 0
80
+ self.scroll_offset = 0
81
+ self.current_table = None
82
+ self.rows = []
83
+ self.columns = []
84
+ self.row_scroll = 0
85
+ self.col_scroll = 0
86
+ self.query_buffer = ""
87
+ self.query_cursor = 0
88
+ self.query_result = None
89
+ self.query_error = None
90
+ self.status = ""
91
+
92
+ db_state = DBState()
93
+
94
+ def get_size():
95
+ try:
96
+ s = os.get_terminal_size()
97
+ return s.columns, s.lines
98
+ except:
99
+ return 80, 24
100
+
101
+ def load_tables():
102
+ try:
103
+ conn = sqlite3.connect(db_path)
104
+ cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
105
+ db_state.tables = [row[0] for row in cursor.fetchall()]
106
+ conn.close()
107
+ except Exception as e:
108
+ db_state.status = f"Error: {e}"
109
+
110
+ def load_table_data(table_name, limit=100):
111
+ try:
112
+ conn = sqlite3.connect(db_path)
113
+ cursor = conn.execute(f"PRAGMA table_info('{table_name}')")
114
+ db_state.columns = [row[1] for row in cursor.fetchall()]
115
+ cursor = conn.execute(f'SELECT * FROM "{table_name}" LIMIT {limit}')
116
+ db_state.rows = cursor.fetchall()
117
+ conn.close()
118
+ db_state.current_table = table_name
119
+ db_state.row_scroll = 0
120
+ db_state.col_scroll = 0
121
+ except Exception as e:
122
+ db_state.status = f"Error: {e}"
123
+
124
+ def run_query(sql):
125
+ try:
126
+ conn = sqlite3.connect(db_path)
127
+ cursor = conn.execute(sql)
128
+ if sql.strip().upper().startswith('SELECT'):
129
+ db_state.columns = [desc[0] for desc in cursor.description] if cursor.description else []
130
+ db_state.rows = cursor.fetchall()
131
+ db_state.query_result = f"Returned {len(db_state.rows)} rows"
132
+ else:
133
+ conn.commit()
134
+ db_state.query_result = f"Affected {cursor.rowcount} rows"
135
+ conn.close()
136
+ db_state.query_error = None
137
+ db_state.row_scroll = 0
138
+ db_state.col_scroll = 0
139
+ except Exception as e:
140
+ db_state.query_error = str(e)
141
+ db_state.query_result = None
142
+
143
+ def render_screen():
144
+ width, height = get_size()
145
+ out = []
146
+ out.append("\033[2J\033[H")
147
+
148
+ header = f" NQL: {os.path.basename(db_path)} "
149
+ out.append(f"\033[1;1H\033[7;1m{'=' * width}\033[0m")
150
+ out.append(f"\033[1;{(width - len(header)) // 2}H\033[7;1m{header}\033[0m")
151
+
152
+ modes = ['Tables', 'Query']
153
+ tab_str = ""
154
+ for i, m in enumerate(modes):
155
+ active = (i == 0 and db_state.mode in ['tables', 'rows']) or (i == 1 and db_state.mode == 'query')
156
+ if active:
157
+ tab_str += f"\033[7m [{m}] \033[0m"
158
+ else:
159
+ tab_str += f" [{m}] "
160
+ out.append(f"\033[2;2H{tab_str}")
161
+ out.append(f"\033[3;1H\033[90m{'─' * width}\033[0m")
162
+
163
+ if db_state.mode == 'tables':
164
+ render_tables(out, width, height)
165
+ elif db_state.mode == 'rows':
166
+ render_rows(out, width, height)
167
+ elif db_state.mode == 'query':
168
+ render_query(out, width, height)
169
+
170
+ if db_state.status:
171
+ out.append(f"\033[{height-2};2H\033[33m{db_state.status[:width-4]}\033[0m")
172
+
173
+ if db_state.mode == 'tables':
174
+ footer = "[j/k] Navigate [Enter] View [q] Query [Esc] Quit"
175
+ elif db_state.mode == 'rows':
176
+ footer = "[j/k] Rows [h/l] Cols [Esc] Back [q] Query"
177
+ else:
178
+ footer = "[Enter] Run [j/k] Scroll [h/l] Cols [Esc] Tables"
179
+ out.append(f"\033[{height};1H\033[90m{footer[:width]}\033[0m")
180
+
181
+ sys.stdout.write(''.join(out))
182
+ sys.stdout.flush()
183
+
184
+ def render_tables(out, width, height):
185
+ visible_height = height - 8
186
+ visible = db_state.tables[db_state.scroll_offset:db_state.scroll_offset + visible_height]
187
+
188
+ out.append(f"\033[4;2H\033[1mTables ({len(db_state.tables)}):\033[0m")
189
+
190
+ row = 5
191
+ for i, table in enumerate(visible):
192
+ idx = i + db_state.scroll_offset
193
+ if idx == db_state.selected_idx:
194
+ out.append(f"\033[{row};4H\033[7m> {table}\033[0m")
195
+ else:
196
+ out.append(f"\033[{row};4H {table}")
197
+ row += 1
198
+
199
+ if not db_state.tables:
200
+ out.append(f"\033[5;4H\033[90mNo tables found.\033[0m")
201
+
202
+ def render_rows(out, width, height):
203
+ visible_height = height - 9
204
+ col_width = 20
205
+
206
+ out.append(f"\033[4;2H\033[1m{db_state.current_table}\033[0m ({len(db_state.rows)} rows)")
207
+
208
+ if db_state.columns:
209
+ visible_cols = db_state.columns[db_state.col_scroll:db_state.col_scroll + (width // col_width)]
210
+ header_str = ""
211
+ for col in visible_cols:
212
+ header_str += f"{col[:col_width-1]:<{col_width}}"
213
+ out.append(f"\033[5;2H\033[1m{header_str[:width-4]}\033[0m")
214
+ out.append(f"\033[6;1H\033[90m{'─' * width}\033[0m")
215
+
216
+ visible_rows = db_state.rows[db_state.row_scroll:db_state.row_scroll + visible_height]
217
+ row = 7
218
+ for data_row in visible_rows:
219
+ visible_cells = data_row[db_state.col_scroll:db_state.col_scroll + (width // col_width)]
220
+ row_str = ""
221
+ for cell in visible_cells:
222
+ cell_str = str(cell) if cell is not None else "NULL"
223
+ if len(cell_str) > col_width - 1:
224
+ cell_str = cell_str[:col_width-2] + "…"
225
+ row_str += f"{cell_str:<{col_width}}"
226
+ out.append(f"\033[{row};2H{row_str[:width-4]}")
227
+ row += 1
228
+
229
+ if len(db_state.rows) > visible_height:
230
+ pct = int((db_state.row_scroll / max(1, len(db_state.rows) - visible_height)) * 100)
231
+ out.append(f"\033[4;{width-8}H\033[90m[{pct}%]\033[0m")
232
+
233
+ def render_query(out, width, height):
234
+ out.append(f"\033[4;2H\033[1mSQL:\033[0m")
235
+ out.append(f"\033[5;2H> {db_state.query_buffer}\033[7m \033[0m")
236
+
237
+ if db_state.query_error:
238
+ out.append(f"\033[7;2H\033[31mError: {db_state.query_error[:width-10]}\033[0m")
239
+ elif db_state.query_result:
240
+ out.append(f"\033[7;2H\033[32m{db_state.query_result}\033[0m")
241
+
242
+ if db_state.columns and db_state.rows:
243
+ col_width = 20
244
+ visible_cols = db_state.columns[db_state.col_scroll:db_state.col_scroll + (width // col_width)]
245
+ header_str = ""
246
+ for col in visible_cols:
247
+ header_str += f"{col[:col_width-1]:<{col_width}}"
248
+ out.append(f"\033[9;2H\033[1m{header_str[:width-4]}\033[0m")
249
+ out.append(f"\033[10;1H\033[90m{'─' * width}\033[0m")
250
+
251
+ visible_height = height - 14
252
+ visible_rows = db_state.rows[db_state.row_scroll:db_state.row_scroll + visible_height]
253
+ row = 11
254
+ for data_row in visible_rows:
255
+ visible_cells = data_row[db_state.col_scroll:db_state.col_scroll + (width // col_width)]
256
+ row_str = ""
257
+ for cell in visible_cells:
258
+ cell_str = str(cell) if cell is not None else "NULL"
259
+ if len(cell_str) > col_width - 1:
260
+ cell_str = cell_str[:col_width-2] + "…"
261
+ row_str += f"{cell_str:<{col_width}}"
262
+ out.append(f"\033[{row};2H{row_str[:width-4]}")
263
+ row += 1
264
+
265
+ def handle_input(c):
266
+ if c == '\x1b':
267
+ if select.select([fd], [], [], 0.1)[0]:
268
+ c2 = os.read(fd, 1).decode('latin-1')
269
+ if c2 == '[':
270
+ c3 = os.read(fd, 1).decode('latin-1')
271
+ if c3 == 'A':
272
+ if db_state.mode == 'tables':
273
+ move_up()
274
+ else:
275
+ db_state.row_scroll = max(0, db_state.row_scroll - 1)
276
+ elif c3 == 'B':
277
+ if db_state.mode == 'tables':
278
+ move_down()
279
+ else:
280
+ db_state.row_scroll = min(max(0, len(db_state.rows) - 1), db_state.row_scroll + 1)
281
+ elif c3 == 'C':
282
+ db_state.col_scroll = min(max(0, len(db_state.columns) - 1), db_state.col_scroll + 1)
283
+ elif c3 == 'D':
284
+ db_state.col_scroll = max(0, db_state.col_scroll - 1)
285
+ else:
286
+ # Plain ESC - go back or exit
287
+ if db_state.mode == 'rows':
288
+ db_state.mode = 'tables'
289
+ elif db_state.mode == 'query':
290
+ db_state.mode = 'tables'
291
+ # In tables mode, ESC does nothing (use 'q' to quit)
292
+ return True
293
+
294
+ if c == 'Q': # Shift+Q to quit
295
+ return False
296
+
297
+ if db_state.mode == 'query':
298
+ return handle_query_input(c)
299
+
300
+ if c == 'q':
301
+ db_state.mode = 'query'
302
+ return True
303
+
304
+ if c == 'k':
305
+ if db_state.mode == 'tables':
306
+ move_up()
307
+ else:
308
+ db_state.row_scroll = max(0, db_state.row_scroll - 1)
309
+ elif c == 'j':
310
+ if db_state.mode == 'tables':
311
+ move_down()
312
+ else:
313
+ db_state.row_scroll = min(max(0, len(db_state.rows) - 1), db_state.row_scroll + 1)
314
+ elif c == 'h':
315
+ db_state.col_scroll = max(0, db_state.col_scroll - 1)
316
+ elif c == 'l':
317
+ db_state.col_scroll = min(max(0, len(db_state.columns) - 1), db_state.col_scroll + 1)
318
+ elif c == '\r' or c == '\n':
319
+ if db_state.mode == 'tables' and db_state.tables:
320
+ table = db_state.tables[db_state.selected_idx]
321
+ load_table_data(table)
322
+ db_state.mode = 'rows'
323
+
324
+ return True
325
+
326
+ def handle_query_input(c):
327
+ # In query mode, all printable chars go to the buffer.
328
+ # Use arrow keys (handled in escape sequence block) for result navigation.
329
+ if c == '\r' or c == '\n':
330
+ if db_state.query_buffer.strip():
331
+ run_query(db_state.query_buffer)
332
+ return True
333
+
334
+ if c == '\x7f' or c == '\x08':
335
+ if db_state.query_cursor > 0:
336
+ db_state.query_buffer = db_state.query_buffer[:db_state.query_cursor-1] + db_state.query_buffer[db_state.query_cursor:]
337
+ db_state.query_cursor -= 1
338
+ elif c >= ' ' and c <= '~':
339
+ db_state.query_buffer = db_state.query_buffer[:db_state.query_cursor] + c + db_state.query_buffer[db_state.query_cursor:]
340
+ db_state.query_cursor += 1
341
+
342
+ return True
343
+
344
+ def move_up():
345
+ db_state.selected_idx = max(0, db_state.selected_idx - 1)
346
+ if db_state.selected_idx < db_state.scroll_offset:
347
+ db_state.scroll_offset = db_state.selected_idx
348
+
349
+ def move_down():
350
+ _, height = get_size()
351
+ visible_height = height - 8
352
+ db_state.selected_idx = min(max(0, len(db_state.tables) - 1), db_state.selected_idx + 1)
353
+ if db_state.selected_idx >= db_state.scroll_offset + visible_height:
354
+ db_state.scroll_offset = db_state.selected_idx - visible_height + 1
355
+
356
+ load_tables()
357
+
358
+ fd = sys.stdin.fileno()
359
+ old_settings = termios.tcgetattr(fd)
360
+
361
+ try:
362
+ tty.setcbreak(fd)
363
+ sys.stdout.write('\033[?25l')
364
+ render_screen()
365
+
366
+ while True:
367
+ c = os.read(fd, 1).decode('latin-1')
368
+ if not handle_input(c):
369
+ break
370
+ render_screen()
371
+
372
+ finally:
373
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
374
+ sys.stdout.write('\033[?25h')
375
+ sys.stdout.write('\033[2J\033[H')
376
+ sys.stdout.flush()
377
+
378
+ context['output'] = "NQL closed."
379
+
380
+ elif list_models:
381
+ if not os.path.exists(models_dir):
382
+ context['output'] = "No models directory found at " + models_dir
383
+ else:
384
+ models_path = Path(models_dir)
385
+ sql_files = list(models_path.glob("**/*.sql"))
386
+ if not sql_files:
387
+ context['output'] = "No .sql models found in " + models_dir
388
+ else:
389
+ lines = ["Available NQL models in " + models_dir + ":", ""]
390
+ for f in sorted(sql_files):
391
+ rel = f.relative_to(models_path)
392
+ with open(f, 'r') as fh:
393
+ content = fh.read()
394
+ has_nql = "nql." in content
395
+ marker = " [NQL]" if has_nql else ""
396
+ lines.append(" " + str(rel) + marker)
397
+ context['output'] = "\n".join(lines)
398
+
399
+ elif install_cron:
400
+ import subprocess
401
+ parts = install_cron.split()
402
+ if len(parts) < 5:
403
+ context['output'] = "Error: cron expression must have 5 fields"
404
+ else:
405
+ cron_time = " ".join(parts[:5])
406
+ cron_model = parts[5] if len(parts) > 5 else ""
407
+ nql_cmd = "npc nql"
408
+ if cron_model:
409
+ nql_cmd += " model=" + cron_model
410
+ nql_cmd += " models_dir=" + models_dir + " db=" + db_path
411
+ if schema:
412
+ nql_cmd += " schema=" + schema
413
+ cron_line = cron_time + " " + nql_cmd
414
+ try:
415
+ result = subprocess.run(['crontab', '-l'], capture_output=True, text=True)
416
+ current = result.stdout if result.returncode == 0 else ""
417
+ except:
418
+ current = ""
419
+ if nql_cmd in current:
420
+ context['output'] = "Cron job already exists"
421
+ else:
422
+ new_crontab = current.rstrip() + "\n" + cron_line + "\n"
423
+ proc = subprocess.run(['crontab', '-'], input=new_crontab, text=True, capture_output=True)
424
+ if proc.returncode == 0:
425
+ context['output'] = "Installed: " + cron_line
426
+ else:
427
+ context['output'] = "Failed: " + proc.stderr
428
+
429
+ else:
430
+ # Run models (non-interactive)
431
+ try:
432
+ from npcpy.sql.npcsql import ModelCompiler
433
+ except ImportError:
434
+ context['output'] = "Error: npcpy.sql.npcsql not found."
435
+
436
+ if not os.path.exists(models_dir):
437
+ context['output'] = "Models directory not found: " + models_dir
438
+ else:
439
+ try:
440
+ compiler = ModelCompiler(
441
+ models_dir=models_dir,
442
+ target_engine=db_path,
443
+ npc_directory=npc_dir,
444
+ target_schema=schema if schema else None
445
+ )
446
+ if model_name:
447
+ compiler.discover_models()
448
+ if model_name not in compiler.models:
449
+ context['output'] = "Model not found: " + model_name
450
+ else:
451
+ df = compiler.execute_model(model_name)
452
+ context['output'] = f"Model '{model_name}' done. Rows: {len(df)}"
453
+ else:
454
+ results = compiler.run_all_models()
455
+ lines = ["NQL complete:"]
456
+ for name, df in results.items():
457
+ lines.append(f" {name}: {len(df)} rows")
458
+ context['output'] = "\n".join(lines)
459
+ except Exception as e:
460
+ context['output'] = "NQL Error: " + str(e)