npcsh 1.1.19__py3-none-any.whl → 1.1.20__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 (144) hide show
  1. npcsh/_state.py +11 -7
  2. npcsh/npc_team/jinxs/bin/config_tui.jinx +3 -2
  3. npcsh/npc_team/jinxs/bin/jinxs.jinx +407 -0
  4. npcsh/npc_team/jinxs/bin/kg.jinx +941 -0
  5. npcsh/npc_team/jinxs/bin/memories.jinx +3 -2
  6. npcsh/npc_team/jinxs/bin/models.jinx +343 -0
  7. npcsh/npc_team/jinxs/bin/nql.jinx +380 -50
  8. npcsh/npc_team/jinxs/bin/setup.jinx +2 -1
  9. npcsh/npc_team/jinxs/bin/team.jinx +504 -0
  10. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +1 -1
  12. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +1 -1
  13. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +1 -1
  14. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +1 -1
  15. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +1 -1
  16. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +1 -1
  17. npcsh/npc_team/jinxs/modes/alicanto.jinx +1 -1
  18. npcsh/npc_team/jinxs/modes/arxiv.jinx +1 -1
  19. npcsh/npc_team/jinxs/modes/corca.jinx +1 -1
  20. npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
  21. npcsh/npc_team/jinxs/modes/plonk.jinx +1 -1
  22. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  23. npcsh/npc_team/jinxs/modes/reattach.jinx +1 -1
  24. npcsh/npc_team/jinxs/modes/spool.jinx +1 -1
  25. npcsh/npc_team/jinxs/modes/wander.jinx +1 -1
  26. npcsh/routes.py +8 -2
  27. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.jinx +1 -1
  28. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/arxiv.jinx +1 -1
  29. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/config_tui.jinx +3 -2
  30. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.jinx +1 -1
  31. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/db_search.jinx +1 -1
  32. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/file_search.jinx +1 -1
  33. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.jinx +4 -4
  34. npcsh-1.1.20.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  35. npcsh-1.1.20.data/data/npcsh/npc_team/kg.jinx +941 -0
  36. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kg_search.jinx +1 -1
  37. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_search.jinx +1 -1
  38. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/memories.jinx +3 -2
  39. npcsh-1.1.20.data/data/npcsh/npc_team/models.jinx +343 -0
  40. npcsh-1.1.20.data/data/npcsh/npc_team/nql.jinx +471 -0
  41. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paper_search.jinx +1 -1
  42. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.jinx +1 -1
  43. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/pti.jinx +1 -1
  44. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/reattach.jinx +1 -1
  45. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/semantic_scholar.jinx +1 -1
  46. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/setup.jinx +2 -1
  47. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.jinx +1 -1
  48. npcsh-1.1.20.data/data/npcsh/npc_team/team.jinx +504 -0
  49. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wander.jinx +1 -1
  50. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/web_search.jinx +1 -1
  51. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/METADATA +1 -1
  52. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/RECORD +139 -135
  53. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/entry_points.txt +4 -1
  54. npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
  55. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -331
  56. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  57. npcsh-1.1.19.data/data/npcsh/npc_team/nql.jinx +0 -141
  58. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +0 -327
  59. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  60. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  61. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.png +0 -0
  62. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  63. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  64. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  65. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/build.jinx +0 -0
  66. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/chat.jinx +0 -0
  67. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/click.jinx +0 -0
  68. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  69. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  70. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  71. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  72. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compile.jinx +0 -0
  73. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compress.jinx +0 -0
  74. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  75. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/convene.jinx +0 -0
  76. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.npc +0 -0
  77. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.png +0 -0
  78. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca_example.png +0 -0
  79. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  80. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  81. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  82. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic.npc +0 -0
  83. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic4.png +0 -0
  84. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.npc +0 -0
  85. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.png +0 -0
  86. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/help.jinx +0 -0
  87. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  88. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/init.jinx +0 -0
  89. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  90. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  91. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  92. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  93. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  94. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  95. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
  96. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  97. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/notify.jinx +0 -0
  98. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  99. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  100. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  101. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  102. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/ots.jinx +0 -0
  103. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paste.jinx +0 -0
  104. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.npc +0 -0
  105. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.png +0 -0
  106. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  107. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  108. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/python.jinx +0 -0
  109. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  110. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/roll.jinx +0 -0
  111. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  112. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sample.jinx +0 -0
  113. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  114. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/search.jinx +0 -0
  115. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  116. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/serve.jinx +0 -0
  117. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/set.jinx +0 -0
  118. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sh.jinx +0 -0
  119. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/shh.jinx +0 -0
  120. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  121. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.png +0 -0
  122. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  123. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  124. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.png +0 -0
  125. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sql.jinx +0 -0
  126. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch.jinx +0 -0
  127. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  128. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  129. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switches.jinx +0 -0
  130. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sync.jinx +0 -0
  131. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  132. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  133. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  134. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/usage.jinx +0 -0
  135. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  136. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  137. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wait.jinx +0 -0
  138. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  139. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.jinx +0 -0
  140. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.png +0 -0
  141. {npcsh-1.1.19.data → npcsh-1.1.20.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  142. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/WHEEL +0 -0
  143. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/licenses/LICENSE +0 -0
  144. {npcsh-1.1.19.dist-info → npcsh-1.1.20.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,471 @@
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([sys.stdin], [], [], 0)[0]:
69
+ sys.stdin.read(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[44;37;1m{'=' * width}\033[0m")
150
+ out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;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([sys.stdin], [], [], 0.1)[0]:
268
+ c2 = sys.stdin.read(1)
269
+ if c2 == '[':
270
+ c3 = sys.stdin.read(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
+ if c == 'k':
328
+ db_state.row_scroll = max(0, db_state.row_scroll - 1)
329
+ return True
330
+ if c == 'j':
331
+ db_state.row_scroll = min(max(0, len(db_state.rows) - 1), db_state.row_scroll + 1)
332
+ return True
333
+ if c == 'h':
334
+ db_state.col_scroll = max(0, db_state.col_scroll - 1)
335
+ return True
336
+ if c == 'l':
337
+ db_state.col_scroll = min(max(0, len(db_state.columns) - 1), db_state.col_scroll + 1)
338
+ return True
339
+
340
+ if c == '\r' or c == '\n':
341
+ if db_state.query_buffer.strip():
342
+ run_query(db_state.query_buffer)
343
+ return True
344
+
345
+ if c == '\x7f' or c == '\x08':
346
+ if db_state.query_cursor > 0:
347
+ db_state.query_buffer = db_state.query_buffer[:db_state.query_cursor-1] + db_state.query_buffer[db_state.query_cursor:]
348
+ db_state.query_cursor -= 1
349
+ elif c >= ' ' and c <= '~':
350
+ db_state.query_buffer = db_state.query_buffer[:db_state.query_cursor] + c + db_state.query_buffer[db_state.query_cursor:]
351
+ db_state.query_cursor += 1
352
+
353
+ return True
354
+
355
+ def move_up():
356
+ db_state.selected_idx = max(0, db_state.selected_idx - 1)
357
+ if db_state.selected_idx < db_state.scroll_offset:
358
+ db_state.scroll_offset = db_state.selected_idx
359
+
360
+ def move_down():
361
+ _, height = get_size()
362
+ visible_height = height - 8
363
+ db_state.selected_idx = min(max(0, len(db_state.tables) - 1), db_state.selected_idx + 1)
364
+ if db_state.selected_idx >= db_state.scroll_offset + visible_height:
365
+ db_state.scroll_offset = db_state.selected_idx - visible_height + 1
366
+
367
+ load_tables()
368
+
369
+ fd = sys.stdin.fileno()
370
+ old_settings = termios.tcgetattr(fd)
371
+
372
+ try:
373
+ tty.setcbreak(fd)
374
+ sys.stdout.write('\033[?25l')
375
+ render_screen()
376
+
377
+ while True:
378
+ c = sys.stdin.read(1)
379
+ if not handle_input(c):
380
+ break
381
+ render_screen()
382
+
383
+ finally:
384
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
385
+ sys.stdout.write('\033[?25h')
386
+ sys.stdout.write('\033[2J\033[H')
387
+ sys.stdout.flush()
388
+
389
+ context['output'] = "NQL closed."
390
+
391
+ elif list_models:
392
+ if not os.path.exists(models_dir):
393
+ context['output'] = "No models directory found at " + models_dir
394
+ else:
395
+ models_path = Path(models_dir)
396
+ sql_files = list(models_path.glob("**/*.sql"))
397
+ if not sql_files:
398
+ context['output'] = "No .sql models found in " + models_dir
399
+ else:
400
+ lines = ["Available NQL models in " + models_dir + ":", ""]
401
+ for f in sorted(sql_files):
402
+ rel = f.relative_to(models_path)
403
+ with open(f, 'r') as fh:
404
+ content = fh.read()
405
+ has_nql = "nql." in content
406
+ marker = " [NQL]" if has_nql else ""
407
+ lines.append(" " + str(rel) + marker)
408
+ context['output'] = "\n".join(lines)
409
+
410
+ elif install_cron:
411
+ import subprocess
412
+ parts = install_cron.split()
413
+ if len(parts) < 5:
414
+ context['output'] = "Error: cron expression must have 5 fields"
415
+ else:
416
+ cron_time = " ".join(parts[:5])
417
+ cron_model = parts[5] if len(parts) > 5 else ""
418
+ nql_cmd = "npc nql"
419
+ if cron_model:
420
+ nql_cmd += " model=" + cron_model
421
+ nql_cmd += " models_dir=" + models_dir + " db=" + db_path
422
+ if schema:
423
+ nql_cmd += " schema=" + schema
424
+ cron_line = cron_time + " " + nql_cmd
425
+ try:
426
+ result = subprocess.run(['crontab', '-l'], capture_output=True, text=True)
427
+ current = result.stdout if result.returncode == 0 else ""
428
+ except:
429
+ current = ""
430
+ if nql_cmd in current:
431
+ context['output'] = "Cron job already exists"
432
+ else:
433
+ new_crontab = current.rstrip() + "\n" + cron_line + "\n"
434
+ proc = subprocess.run(['crontab', '-'], input=new_crontab, text=True, capture_output=True)
435
+ if proc.returncode == 0:
436
+ context['output'] = "Installed: " + cron_line
437
+ else:
438
+ context['output'] = "Failed: " + proc.stderr
439
+
440
+ else:
441
+ # Run models (non-interactive)
442
+ try:
443
+ from npcpy.sql.npcsql import ModelCompiler
444
+ except ImportError:
445
+ context['output'] = "Error: npcpy.sql.npcsql not found."
446
+
447
+ if not os.path.exists(models_dir):
448
+ context['output'] = "Models directory not found: " + models_dir
449
+ else:
450
+ try:
451
+ compiler = ModelCompiler(
452
+ models_dir=models_dir,
453
+ target_engine=db_path,
454
+ npc_directory=npc_dir,
455
+ target_schema=schema if schema else None
456
+ )
457
+ if model_name:
458
+ compiler.discover_models()
459
+ if model_name not in compiler.models:
460
+ context['output'] = "Model not found: " + model_name
461
+ else:
462
+ df = compiler.execute_model(model_name)
463
+ context['output'] = f"Model '{model_name}' done. Rows: {len(df)}"
464
+ else:
465
+ results = compiler.run_all_models()
466
+ lines = ["NQL complete:"]
467
+ for name, df in results.items():
468
+ lines.append(f" {name}: {len(df)} rows")
469
+ context['output'] = "\n".join(lines)
470
+ except Exception as e:
471
+ context['output'] = "NQL Error: " + str(e)
@@ -225,7 +225,7 @@ steps:
225
225
  line = line[:width+10]
226
226
 
227
227
  if idx == selected:
228
- sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
228
+ sys.stdout.write(f'\033[7;1m>{line}\033[0m')
229
229
  else:
230
230
  sys.stdout.write(f' {line}')
231
231
 
@@ -130,7 +130,7 @@ steps:
130
130
  color = ''
131
131
 
132
132
  if idx == selected:
133
- sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
133
+ sys.stdout.write(f'\033[7;1m>{line.ljust(width-2)}\033[0m')
134
134
  elif color:
135
135
  sys.stdout.write(f'{color}{line}\033[0m')
136
136
  else:
@@ -123,7 +123,7 @@ steps:
123
123
  line = line[:width-1]
124
124
 
125
125
  if idx == selected:
126
- sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
126
+ sys.stdout.write(f'\033[7;1m>{line.ljust(width-2)}\033[0m')
127
127
  else:
128
128
  sys.stdout.write(f'\033[33m{line}\033[0m')
129
129
 
@@ -160,7 +160,7 @@ steps:
160
160
  line = line[:width-2].ljust(width-1)
161
161
 
162
162
  if idx == selected:
163
- sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
163
+ sys.stdout.write(f'\033[7;1m>{line}\033[0m')
164
164
  else:
165
165
  sys.stdout.write(f' {line}')
166
166
 
@@ -181,7 +181,7 @@ steps:
181
181
  line = line[:width-1]
182
182
 
183
183
  if idx == selected:
184
- sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
184
+ sys.stdout.write(f'\033[7;1m>{line}\033[0m')
185
185
  else:
186
186
  sys.stdout.write(f' {line}')
187
187
 
@@ -1,5 +1,6 @@
1
1
  jinx_name: setup
2
2
  description: Interactive setup wizard for npcsh - detect local models, configure defaults
3
+ interactive: true
3
4
  inputs:
4
5
  - skip_detection: ""
5
6
  steps:
@@ -136,7 +137,7 @@ steps:
136
137
  row = 5 + i
137
138
  idx = i + state.scroll_offset
138
139
  if idx == state.selected_idx:
139
- out.append(f"\033[{row};4H\033[47;30m> {model} ({provider})\033[0m")
140
+ out.append(f"\033[{row};4H\033[7m> {model} ({provider})\033[0m")
140
141
  else:
141
142
  out.append(f"\033[{row};4H {model} \033[90m({provider})\033[0m")
142
143
 
@@ -118,7 +118,7 @@ steps:
118
118
  color = ''
119
119
 
120
120
  if idx == selected:
121
- sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
121
+ sys.stdout.write(f'\033[7;1m>{line.ljust(width-2)}\033[0m')
122
122
  elif color:
123
123
  sys.stdout.write(f'{color}{line}\033[0m')
124
124
  else: