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
@@ -1,8 +1,9 @@
1
1
  jinx_name: nql
2
- description: "Run NPC-SQL models with AI-powered transformations. Supports cron scheduling."
2
+ description: "Interactive DB viewer/manager or run NPC-SQL models"
3
+ interactive: true
3
4
  inputs:
4
5
  - models_dir: "~/.npcsh/npc_team/models"
5
- - db: "~/npcsh_history.db"
6
+ - db: ""
6
7
  - model: ""
7
8
  - schema: ""
8
9
  - show: ""
@@ -15,99 +16,436 @@ steps:
15
16
  code: |
16
17
  import os
17
18
  import sys
19
+ import tty
20
+ import termios
21
+ import select
18
22
  from pathlib import Path
19
23
 
20
24
  models_dir = context.get('models_dir') or '~/.npcsh/npc_team/models'
21
- db_path = context.get('db') or '~/npcsh_history.db'
25
+ db_path = context.get('db') or ''
22
26
  model_name = context.get('model') or ''
23
27
  schema = context.get('schema') or ''
24
28
  list_models = context.get('show') or ''
25
- cron_expr = context.get('cron') or ''
26
29
  install_cron = context.get('install_cron') or ''
27
30
 
28
31
  models_dir = os.path.expanduser(models_dir)
29
- db_path = os.path.expanduser(db_path)
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)
30
39
 
31
40
  # Find NPC team directory
32
41
  npc_dir = None
33
- if os.path.exists('./npc_team'):
42
+ if state and state.team:
43
+ npc_dir = state.team.team_path
44
+ elif os.path.exists('./npc_team'):
34
45
  npc_dir = './npc_team'
35
46
  elif os.path.exists(os.path.expanduser('~/.npcsh/npc_team')):
36
47
  npc_dir = os.path.expanduser('~/.npcsh/npc_team')
37
48
 
38
- # Import npcsql
39
- try:
40
- from npcpy.sql.npcsql import ModelCompiler, SQLModel
41
- except ImportError:
42
- output = "Error: npcpy.sql.npcsql not found. Install npcpy first."
43
- raise SystemExit(1)
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)")
44
207
 
45
- # List models mode
46
- if list_models:
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:
47
392
  if not os.path.exists(models_dir):
48
- output = "No models directory found at " + models_dir
393
+ context['output'] = "No models directory found at " + models_dir
49
394
  else:
50
395
  models_path = Path(models_dir)
51
396
  sql_files = list(models_path.glob("**/*.sql"))
52
397
  if not sql_files:
53
- output = "No .sql models found in " + models_dir
398
+ context['output'] = "No .sql models found in " + models_dir
54
399
  else:
55
400
  lines = ["Available NQL models in " + models_dir + ":", ""]
56
401
  for f in sorted(sql_files):
57
402
  rel = f.relative_to(models_path)
58
- # Check for nql functions
59
403
  with open(f, 'r') as fh:
60
404
  content = fh.read()
61
405
  has_nql = "nql." in content
62
406
  marker = " [NQL]" if has_nql else ""
63
407
  lines.append(" " + str(rel) + marker)
64
- output = "\n".join(lines)
408
+ context['output'] = "\n".join(lines)
65
409
 
66
- # Install cron mode
67
410
  elif install_cron:
68
411
  import subprocess
69
- # Parse cron expression and model
70
412
  parts = install_cron.split()
71
413
  if len(parts) < 5:
72
- output = "Error: cron expression must have 5 fields (min hour day month weekday)"
414
+ context['output'] = "Error: cron expression must have 5 fields"
73
415
  else:
74
416
  cron_time = " ".join(parts[:5])
75
417
  cron_model = parts[5] if len(parts) > 5 else ""
76
-
77
- # Build command
78
418
  nql_cmd = "npc nql"
79
419
  if cron_model:
80
420
  nql_cmd += " model=" + cron_model
81
- nql_cmd += " models_dir=" + models_dir
82
- nql_cmd += " db=" + db_path
421
+ nql_cmd += " models_dir=" + models_dir + " db=" + db_path
83
422
  if schema:
84
423
  nql_cmd += " schema=" + schema
85
-
86
424
  cron_line = cron_time + " " + nql_cmd
87
-
88
- # Get current crontab
89
425
  try:
90
426
  result = subprocess.run(['crontab', '-l'], capture_output=True, text=True)
91
427
  current = result.stdout if result.returncode == 0 else ""
92
428
  except:
93
429
  current = ""
94
-
95
- # Check if already installed
96
430
  if nql_cmd in current:
97
- output = "Cron job already exists for: " + nql_cmd
431
+ context['output'] = "Cron job already exists"
98
432
  else:
99
- # Add new cron line
100
433
  new_crontab = current.rstrip() + "\n" + cron_line + "\n"
101
434
  proc = subprocess.run(['crontab', '-'], input=new_crontab, text=True, capture_output=True)
102
435
  if proc.returncode == 0:
103
- output = "Installed cron job:\n " + cron_line
436
+ context['output'] = "Installed: " + cron_line
104
437
  else:
105
- output = "Failed to install cron: " + proc.stderr
438
+ context['output'] = "Failed: " + proc.stderr
106
439
 
107
- # Run models mode
108
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
+
109
447
  if not os.path.exists(models_dir):
110
- output = "Models directory not found: " + models_dir + "\nCreate it with: mkdir -p " + models_dir
448
+ context['output'] = "Models directory not found: " + models_dir
111
449
  else:
112
450
  try:
113
451
  compiler = ModelCompiler(
@@ -116,26 +454,18 @@ steps:
116
454
  npc_directory=npc_dir,
117
455
  target_schema=schema if schema else None
118
456
  )
119
-
120
457
  if model_name:
121
- # Run specific model
122
458
  compiler.discover_models()
123
459
  if model_name not in compiler.models:
124
- available = list(compiler.models.keys())
125
- output = "Model '" + model_name + "' not found. Available: " + str(available)
460
+ context['output'] = "Model not found: " + model_name
126
461
  else:
127
- print("Running model: " + model_name)
128
462
  df = compiler.execute_model(model_name)
129
- output = "Model '" + model_name + "' completed. Rows: " + str(len(df))
463
+ context['output'] = f"Model '{model_name}' done. Rows: {len(df)}"
130
464
  else:
131
- # Run all models in dependency order
132
465
  results = compiler.run_all_models()
133
- lines = ["NQL run complete:", ""]
466
+ lines = ["NQL complete:"]
134
467
  for name, df in results.items():
135
- lines.append(" " + name + ": " + str(len(df)) + " rows")
136
- output = "\n".join(lines)
137
-
468
+ lines.append(f" {name}: {len(df)} rows")
469
+ context['output'] = "\n".join(lines)
138
470
  except Exception as e:
139
- output = "NQL Error: " + str(e)
140
- import traceback
141
- traceback.print_exc()
471
+ context['output'] = "NQL Error: " + str(e)
@@ -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