npcsh 1.1.18__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 (165) hide show
  1. npcsh/_state.py +19 -7
  2. npcsh/benchmark/npcsh_agent.py +47 -16
  3. npcsh/config.py +1 -0
  4. npcsh/diff_viewer.py +452 -0
  5. npcsh/npc_team/jinxs/bin/config_tui.jinx +300 -0
  6. npcsh/npc_team/jinxs/bin/jinxs.jinx +407 -0
  7. npcsh/npc_team/jinxs/bin/kg.jinx +941 -0
  8. npcsh/npc_team/jinxs/bin/memories.jinx +317 -0
  9. npcsh/npc_team/jinxs/bin/models.jinx +343 -0
  10. npcsh/npc_team/jinxs/bin/nql.jinx +380 -50
  11. npcsh/npc_team/jinxs/bin/setup.jinx +241 -0
  12. npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
  13. npcsh/npc_team/jinxs/bin/team.jinx +504 -0
  14. npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
  15. npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
  16. npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
  17. npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
  18. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
  19. npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
  20. npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
  21. npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
  22. npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
  23. npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
  24. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
  25. npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
  26. npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
  27. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
  28. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
  29. npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
  30. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
  31. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +1 -1
  32. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +1 -1
  33. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +1 -1
  34. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +1 -1
  35. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +1 -1
  36. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +1 -1
  37. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +1 -1
  38. npcsh/npc_team/jinxs/modes/alicanto.jinx +1 -1
  39. npcsh/npc_team/jinxs/modes/arxiv.jinx +1 -1
  40. npcsh/npc_team/jinxs/modes/corca.jinx +1 -1
  41. npcsh/npc_team/jinxs/modes/guac.jinx +4 -6
  42. npcsh/npc_team/jinxs/modes/plonk.jinx +1 -1
  43. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  44. npcsh/npc_team/jinxs/modes/reattach.jinx +1 -1
  45. npcsh/npc_team/jinxs/modes/spool.jinx +1 -1
  46. npcsh/npc_team/jinxs/modes/wander.jinx +1 -1
  47. npcsh/routes.py +8 -2
  48. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
  49. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.jinx +1 -1
  50. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/arxiv.jinx +1 -1
  51. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
  52. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
  53. npcsh-1.1.20.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  54. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/confirm.jinx +1 -1
  55. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.jinx +1 -1
  56. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/db_search.jinx +1 -1
  57. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/file_search.jinx +1 -1
  58. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
  59. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.jinx +4 -6
  60. npcsh-1.1.20.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  61. npcsh-1.1.20.data/data/npcsh/npc_team/kg.jinx +941 -0
  62. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kg_search.jinx +1 -1
  63. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
  64. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_search.jinx +1 -1
  65. npcsh-1.1.20.data/data/npcsh/npc_team/memories.jinx +317 -0
  66. npcsh-1.1.20.data/data/npcsh/npc_team/models.jinx +343 -0
  67. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/navigate.jinx +1 -1
  68. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/notify.jinx +1 -1
  69. npcsh-1.1.20.data/data/npcsh/npc_team/nql.jinx +471 -0
  70. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
  71. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paper_search.jinx +1 -1
  72. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.jinx +1 -1
  73. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/pti.jinx +1 -1
  74. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
  75. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/reattach.jinx +1 -1
  76. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
  77. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/semantic_scholar.jinx +1 -1
  78. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/send_message.jinx +1 -1
  79. npcsh-1.1.20.data/data/npcsh/npc_team/setup.jinx +241 -0
  80. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
  81. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.jinx +1 -1
  82. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
  83. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
  84. npcsh-1.1.20.data/data/npcsh/npc_team/sync.jinx +223 -0
  85. npcsh-1.1.20.data/data/npcsh/npc_team/team.jinx +504 -0
  86. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wander.jinx +1 -1
  87. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/web_search.jinx +1 -1
  88. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/write_file.jinx +1 -1
  89. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
  90. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/METADATA +21 -14
  91. npcsh-1.1.20.dist-info/RECORD +248 -0
  92. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/entry_points.txt +7 -0
  93. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -331
  94. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  95. npcsh-1.1.18.data/data/npcsh/npc_team/nql.jinx +0 -141
  96. npcsh-1.1.18.data/data/npcsh/npc_team/sync.jinx +0 -230
  97. npcsh-1.1.18.dist-info/RECORD +0 -235
  98. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  99. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.png +0 -0
  100. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  101. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  102. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  103. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/build.jinx +0 -0
  104. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/chat.jinx +0 -0
  105. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/click.jinx +0 -0
  106. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  107. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  108. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compile.jinx +0 -0
  109. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compress.jinx +0 -0
  110. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/convene.jinx +0 -0
  111. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.npc +0 -0
  112. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.png +0 -0
  113. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca_example.png +0 -0
  114. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  115. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  116. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic.npc +0 -0
  117. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic4.png +0 -0
  118. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.npc +0 -0
  119. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.png +0 -0
  120. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/help.jinx +0 -0
  121. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  122. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/init.jinx +0 -0
  123. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  124. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  125. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  126. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  127. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  128. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
  129. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  130. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  131. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  132. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/ots.jinx +0 -0
  133. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paste.jinx +0 -0
  134. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.npc +0 -0
  135. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.png +0 -0
  136. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  137. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  138. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/python.jinx +0 -0
  139. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sample.jinx +0 -0
  141. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  142. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/search.jinx +0 -0
  143. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/serve.jinx +0 -0
  144. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/set.jinx +0 -0
  145. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sh.jinx +0 -0
  146. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/shh.jinx +0 -0
  147. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  148. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.png +0 -0
  149. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  150. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.png +0 -0
  151. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sql.jinx +0 -0
  152. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch.jinx +0 -0
  153. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switches.jinx +0 -0
  154. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  155. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  156. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  157. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/usage.jinx +0 -0
  158. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  159. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  160. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wait.jinx +0 -0
  161. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.jinx +0 -0
  162. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.png +0 -0
  163. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/WHEEL +0 -0
  164. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/licenses/LICENSE +0 -0
  165. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,317 @@
1
+ jinx_name: memories
2
+ description: Interactive TUI for browsing and managing npcsh memories
3
+ interactive: true
4
+ inputs:
5
+ - scope: ""
6
+ steps:
7
+ - name: memory_browser
8
+ engine: python
9
+ code: |
10
+ import os
11
+ import sys
12
+ import tty
13
+ import termios
14
+ import select
15
+ from datetime import datetime
16
+
17
+ if not sys.stdin.isatty():
18
+ context['output'] = "Memory browser requires an interactive terminal."
19
+ return
20
+
21
+ from npcpy.memory.command_history import CommandHistory
22
+ from npcsh.config import NPCSH_DB_PATH
23
+
24
+ db_path = os.path.expanduser(NPCSH_DB_PATH)
25
+ command_history = CommandHistory(db_path)
26
+
27
+ # ========== State ==========
28
+ class MemoryState:
29
+ def __init__(self):
30
+ self.tab = 0 # 0=All, 1=Pending, 2=Approved, 3=Rejected
31
+ self.tabs = ['All', 'Pending', 'Approved', 'Rejected']
32
+ self.memories = []
33
+ self.selected_idx = 0
34
+ self.scroll_offset = 0
35
+ self.preview_mode = False
36
+ self.status = ""
37
+ self.filters = {
38
+ 'npc': None,
39
+ 'team': None,
40
+ }
41
+
42
+ state = MemoryState()
43
+
44
+ # ========== Helpers ==========
45
+ def get_size():
46
+ try:
47
+ s = os.get_terminal_size()
48
+ return s.columns, s.lines
49
+ except:
50
+ return 80, 24
51
+
52
+ def load_memories():
53
+ """Load memories based on current tab filter."""
54
+ state.memories = []
55
+
56
+ try:
57
+ with command_history.engine.connect() as conn:
58
+ status_filter = {
59
+ 0: None, # All
60
+ 1: 'pending',
61
+ 2: 'approved',
62
+ 3: 'rejected'
63
+ }.get(state.tab)
64
+
65
+ query = "SELECT id, created_at, npc_name, team_name, scope, original_memory, final_memory, status FROM memories"
66
+ conditions = []
67
+
68
+ if status_filter:
69
+ conditions.append(f"status = '{status_filter}'")
70
+ if state.filters['npc']:
71
+ conditions.append(f"npc_name = '{state.filters['npc']}'")
72
+ if state.filters['team']:
73
+ conditions.append(f"team_name = '{state.filters['team']}'")
74
+
75
+ if conditions:
76
+ query += " WHERE " + " AND ".join(conditions)
77
+
78
+ query += " ORDER BY created_at DESC LIMIT 100"
79
+
80
+ from sqlalchemy import text
81
+ result = conn.execute(text(query))
82
+ for row in result:
83
+ state.memories.append({
84
+ 'id': row[0],
85
+ 'created_at': row[1],
86
+ 'npc': row[2],
87
+ 'team': row[3],
88
+ 'scope': row[4],
89
+ 'original': row[5],
90
+ 'final': row[6],
91
+ 'status': row[7]
92
+ })
93
+ except Exception as e:
94
+ state.status = f"Error loading memories: {e}"
95
+
96
+ def update_memory_status(memory_id, new_status):
97
+ """Update a memory's status."""
98
+ try:
99
+ command_history.update_memory_status(memory_id, new_status)
100
+ state.status = f"Memory {memory_id} marked as {new_status}"
101
+ load_memories()
102
+ except Exception as e:
103
+ state.status = f"Error: {e}"
104
+
105
+ def format_date(dt_str):
106
+ """Format datetime string for display."""
107
+ if not dt_str:
108
+ return ""
109
+ try:
110
+ if isinstance(dt_str, str):
111
+ dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
112
+ else:
113
+ dt = dt_str
114
+ return dt.strftime('%m-%d %H:%M')
115
+ except:
116
+ return str(dt_str)[:10]
117
+
118
+ # ========== Rendering ==========
119
+ def render_screen():
120
+ width, height = get_size()
121
+ out = []
122
+ out.append("\033[2J\033[H")
123
+
124
+ # Header
125
+ header = " Memory Browser "
126
+ out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
127
+ out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;1m{header}\033[0m")
128
+
129
+ # Tabs
130
+ tab_str = ""
131
+ for i, tab in enumerate(state.tabs):
132
+ count = sum(1 for m in state.memories if state.tab == 0 or True) # Will filter properly
133
+ if i == state.tab:
134
+ tab_str += f"\033[7m [{tab}] \033[0m"
135
+ else:
136
+ tab_str += f" [{tab}] "
137
+ out.append(f"\033[2;2H{tab_str}")
138
+
139
+ # Separator
140
+ out.append(f"\033[3;1H\033[90m{'─' * width}\033[0m")
141
+
142
+ if state.preview_mode and state.memories:
143
+ render_preview(out, width, height)
144
+ else:
145
+ render_list(out, width, height)
146
+
147
+ # Status
148
+ if state.status:
149
+ out.append(f"\033[{height-2};2H\033[33m{state.status[:width-4]}\033[0m")
150
+
151
+ # Footer
152
+ if state.preview_mode:
153
+ footer = "[Esc] Back [a] Approve [x] Reject [j/k] Prev/Next"
154
+ else:
155
+ footer = "[Tab] Filter [j/k] Navigate [p] Preview [a] Approve [x] Reject [q] Quit"
156
+ out.append(f"\033[{height};1H\033[90m{footer[:width]}\033[0m")
157
+
158
+ sys.stdout.write(''.join(out))
159
+ sys.stdout.flush()
160
+
161
+ def render_list(out, width, height):
162
+ """Render memory list."""
163
+ visible_height = height - 7
164
+ visible = state.memories[state.scroll_offset:state.scroll_offset + visible_height]
165
+
166
+ if not state.memories:
167
+ out.append(f"\033[5;4H\033[90mNo memories found.\033[0m")
168
+ return
169
+
170
+ row = 4
171
+ for i, mem in enumerate(visible):
172
+ idx = i + state.scroll_offset
173
+
174
+ # Status indicator
175
+ status_icon = {
176
+ 'pending': '\033[33m○\033[0m',
177
+ 'approved': '\033[32m●\033[0m',
178
+ 'rejected': '\033[31m✗\033[0m'
179
+ }.get(mem['status'], '?')
180
+
181
+ # Format line
182
+ date_str = format_date(mem['created_at'])
183
+ npc_str = mem['npc'][:8] if mem['npc'] else '-'
184
+ content = (mem['final'] or mem['original'] or '')[:width-35]
185
+ content = content.replace('\n', ' ')
186
+
187
+ if idx == state.selected_idx:
188
+ out.append(f"\033[{row};2H\033[7m{status_icon} {date_str} {npc_str:<8} {content}\033[0m")
189
+ else:
190
+ out.append(f"\033[{row};2H{status_icon} {date_str} \033[90m{npc_str:<8}\033[0m {content}")
191
+
192
+ row += 1
193
+
194
+ # Scroll indicator
195
+ if len(state.memories) > visible_height:
196
+ pct = int((state.scroll_offset / max(1, len(state.memories) - visible_height)) * 100)
197
+ out.append(f"\033[4;{width-6}H\033[90m[{pct}%]\033[0m")
198
+
199
+ def render_preview(out, width, height):
200
+ """Render memory preview."""
201
+ if not state.memories or state.selected_idx >= len(state.memories):
202
+ return
203
+
204
+ mem = state.memories[state.selected_idx]
205
+
206
+ row = 4
207
+ out.append(f"\033[{row};2H\033[1mMemory #{mem['id']}\033[0m")
208
+ row += 1
209
+
210
+ # Metadata
211
+ status_color = {'pending': '33', 'approved': '32', 'rejected': '31'}.get(mem['status'], '0')
212
+ out.append(f"\033[{row};2HStatus: \033[{status_color}m{mem['status']}\033[0m")
213
+ row += 1
214
+ out.append(f"\033[{row};2HDate: {format_date(mem['created_at'])}")
215
+ row += 1
216
+ out.append(f"\033[{row};2HNPC: {mem['npc'] or '-'} Team: {mem['team'] or '-'} Scope: {mem['scope'] or '-'}")
217
+ row += 2
218
+
219
+ # Content
220
+ out.append(f"\033[{row};2H\033[1mContent:\033[0m")
221
+ row += 1
222
+
223
+ content = mem['final'] or mem['original'] or '(empty)'
224
+ content_lines = content.split('\n')
225
+ for line in content_lines[:height-row-3]:
226
+ out.append(f"\033[{row};4H{line[:width-6]}")
227
+ row += 1
228
+
229
+ # ========== Input Handling ==========
230
+ def handle_input(c):
231
+ if c == 'q':
232
+ return False
233
+
234
+ if c == '\x1b': # Escape
235
+ if select.select([sys.stdin], [], [], 0.05)[0]:
236
+ c2 = sys.stdin.read(1)
237
+ if c2 == '[':
238
+ c3 = sys.stdin.read(1)
239
+ if c3 == 'A': # Up
240
+ move_up()
241
+ elif c3 == 'B': # Down
242
+ move_down()
243
+ else:
244
+ if state.preview_mode:
245
+ state.preview_mode = False
246
+ return True
247
+
248
+ if c == '\t': # Tab - cycle tabs
249
+ state.tab = (state.tab + 1) % len(state.tabs)
250
+ state.selected_idx = 0
251
+ state.scroll_offset = 0
252
+ load_memories()
253
+ state.status = ""
254
+
255
+ elif c == 'k':
256
+ move_up()
257
+ elif c == 'j':
258
+ move_down()
259
+ elif c == 'p' or c == '\r' or c == '\n':
260
+ if state.memories:
261
+ state.preview_mode = not state.preview_mode
262
+ elif c == 'a':
263
+ approve_current()
264
+ elif c == 'x':
265
+ reject_current()
266
+
267
+ return True
268
+
269
+ def move_up():
270
+ state.selected_idx = max(0, state.selected_idx - 1)
271
+ if state.selected_idx < state.scroll_offset:
272
+ state.scroll_offset = state.selected_idx
273
+ state.status = ""
274
+
275
+ def move_down():
276
+ _, height = get_size()
277
+ visible_height = height - 7
278
+ state.selected_idx = min(len(state.memories) - 1, state.selected_idx + 1)
279
+ if state.selected_idx >= state.scroll_offset + visible_height:
280
+ state.scroll_offset = state.selected_idx - visible_height + 1
281
+ state.status = ""
282
+
283
+ def approve_current():
284
+ if state.memories and state.selected_idx < len(state.memories):
285
+ mem = state.memories[state.selected_idx]
286
+ update_memory_status(mem['id'], 'approved')
287
+
288
+ def reject_current():
289
+ if state.memories and state.selected_idx < len(state.memories):
290
+ mem = state.memories[state.selected_idx]
291
+ update_memory_status(mem['id'], 'rejected')
292
+
293
+ # ========== Main Loop ==========
294
+ load_memories()
295
+
296
+ fd = sys.stdin.fileno()
297
+ old_settings = termios.tcgetattr(fd)
298
+
299
+ try:
300
+ tty.setcbreak(fd)
301
+ sys.stdout.write('\033[?25l') # Hide cursor
302
+
303
+ render_screen()
304
+
305
+ while True:
306
+ c = sys.stdin.read(1)
307
+ if not handle_input(c):
308
+ break
309
+ render_screen()
310
+
311
+ finally:
312
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
313
+ sys.stdout.write('\033[?25h') # Show cursor
314
+ sys.stdout.write('\033[2J\033[H') # Clear screen
315
+ sys.stdout.flush()
316
+
317
+ context['output'] = "Memory browser closed."
@@ -0,0 +1,343 @@
1
+ jinx_name: models
2
+ description: Interactive model browser - detect available models and set active defaults
3
+ interactive: true
4
+ inputs: []
5
+ steps:
6
+ - name: model_browser
7
+ engine: python
8
+ code: |
9
+ import os
10
+ import sys
11
+ import tty
12
+ import termios
13
+ import select
14
+
15
+ if not sys.stdin.isatty():
16
+ context['output'] = "Models TUI requires an interactive terminal."
17
+
18
+ else:
19
+ from npcpy.npc_sysenv import get_locally_available_models
20
+ from npcsh.config import set_npcsh_config_value
21
+
22
+ LOCAL_PROVIDERS = {'ollama', 'llamacpp', 'lmstudio', 'mlx', 'lora'}
23
+ CLOUD_PROVIDERS = {'openai', 'anthropic', 'gemini', 'deepseek'}
24
+
25
+ class TUIState:
26
+ def __init__(self):
27
+ self.tab = 0
28
+ self.tabs = ['All', 'Local', 'Cloud', 'Active']
29
+ self.sel = 0
30
+ self.scroll = 0
31
+ self.status = ""
32
+ self.models = [] # list of (provider, model_id)
33
+ self.filtered = [] # filtered view
34
+ self.chat_model = ''
35
+ self.chat_provider = ''
36
+ self.vision_model = ''
37
+ self.vision_provider = ''
38
+ self.embed_model = ''
39
+ self.embed_provider = ''
40
+ self.reasoning_model = ''
41
+ self.reasoning_provider = ''
42
+
43
+ ui = TUIState()
44
+
45
+ def term_size():
46
+ try:
47
+ s = os.get_terminal_size()
48
+ return s.columns, s.lines
49
+ except:
50
+ return 80, 24
51
+
52
+ def load_active():
53
+ ui.chat_model = os.environ.get('NPCSH_CHAT_MODEL', '')
54
+ ui.chat_provider = os.environ.get('NPCSH_CHAT_PROVIDER', '')
55
+ ui.vision_model = os.environ.get('NPCSH_VISION_MODEL', '')
56
+ ui.vision_provider = os.environ.get('NPCSH_VISION_PROVIDER', '')
57
+ ui.embed_model = os.environ.get('NPCSH_EMBEDDING_MODEL', '')
58
+ ui.embed_provider = os.environ.get('NPCSH_EMBEDDING_PROVIDER', '')
59
+ ui.reasoning_model = os.environ.get('NPCSH_REASONING_MODEL', '')
60
+ ui.reasoning_provider = os.environ.get('NPCSH_REASONING_PROVIDER', '')
61
+
62
+ def detect_models():
63
+ ui.status = "Detecting models..."
64
+ try:
65
+ models_dict = get_locally_available_models('.', airplane_mode=False)
66
+ except Exception as e:
67
+ models_dict = {}
68
+ ui.status = f"Detection error: {e}"
69
+ ui.models = []
70
+ if models_dict:
71
+ for model_id, provider in sorted(models_dict.items(), key=lambda x: (x[1], x[0])):
72
+ ui.models.append((provider, model_id))
73
+ if ui.models and not ui.status.startswith("Detection error"):
74
+ ui.status = f"Found {len(ui.models)} models"
75
+ apply_filter()
76
+
77
+ def apply_filter():
78
+ if ui.tab == 0:
79
+ ui.filtered = list(ui.models)
80
+ elif ui.tab == 1:
81
+ ui.filtered = [(p, m) for p, m in ui.models if p in LOCAL_PROVIDERS]
82
+ elif ui.tab == 2:
83
+ ui.filtered = [(p, m) for p, m in ui.models if p in CLOUD_PROVIDERS]
84
+ elif ui.tab == 3:
85
+ active = set()
86
+ if ui.chat_model and ui.chat_provider:
87
+ active.add((ui.chat_provider, ui.chat_model))
88
+ if ui.vision_model and ui.vision_provider:
89
+ active.add((ui.vision_provider, ui.vision_model))
90
+ if ui.embed_model and ui.embed_provider:
91
+ active.add((ui.embed_provider, ui.embed_model))
92
+ if ui.reasoning_model and ui.reasoning_provider:
93
+ active.add((ui.reasoning_provider, ui.reasoning_model))
94
+ ui.filtered = [(p, m) for p, m in ui.models if (p, m) in active]
95
+ ui.sel = min(ui.sel, max(0, len(ui.filtered) - 1))
96
+ ui.scroll = min(ui.scroll, max(0, ui.sel))
97
+
98
+ def model_roles(provider, model_id):
99
+ roles = []
100
+ if model_id == ui.chat_model and provider == ui.chat_provider:
101
+ roles.append('chat')
102
+ if model_id == ui.vision_model and provider == ui.vision_provider:
103
+ roles.append('vision')
104
+ if model_id == ui.embed_model and provider == ui.embed_provider:
105
+ roles.append('embed')
106
+ if model_id == ui.reasoning_model and provider == ui.reasoning_provider:
107
+ roles.append('reasoning')
108
+ return roles
109
+
110
+ # -- rendering -----------------------------------------------
111
+ def wline(row, text):
112
+ return f"\033[{row};1H\033[K{text}"
113
+
114
+ def render():
115
+ W, H = term_size()
116
+ out = []
117
+
118
+ out.append("\033[H")
119
+
120
+ # -- header --
121
+ hdr = " Models "
122
+ pad = '=' * W
123
+ out.append(wline(1, f"\033[44;37;1m{pad}\033[0m"))
124
+ out.append(f"\033[1;{max(1,(W - len(hdr)) // 2)}H\033[44;37;1m{hdr}\033[0m")
125
+
126
+ # -- tabs --
127
+ tb = ""
128
+ for i, t in enumerate(ui.tabs):
129
+ if i == ui.tab:
130
+ tb += f"\033[7;1m [{t}] \033[0m"
131
+ else:
132
+ tb += f" {t} "
133
+ out.append(wline(2, f" {tb}"))
134
+ out.append(wline(3, f"\033[90m{'─' * W}\033[0m"))
135
+
136
+ # -- column header --
137
+ prov_w = 14
138
+ model_w = max(24, W - prov_w - 24)
139
+ col_hdr = f" {'Provider':<{prov_w}}{'Model':<{model_w}}Status"
140
+ out.append(wline(4, f"\033[1m{col_hdr[:W]}\033[0m"))
141
+
142
+ # -- body --
143
+ body_start = 5
144
+ body_end = H - 4
145
+ body_h = body_end - body_start + 1
146
+
147
+ vis = ui.filtered[ui.scroll:ui.scroll + body_h]
148
+ for r in range(body_h):
149
+ row = body_start + r
150
+ idx = r + ui.scroll
151
+ if r >= len(vis):
152
+ out.append(wline(row, ""))
153
+ continue
154
+ provider, model_id = vis[r]
155
+ roles = model_roles(provider, model_id)
156
+ if roles:
157
+ role_str = "\033[32m* " + ', '.join(roles) + "\033[0m"
158
+ role_plain = "* " + ', '.join(roles)
159
+ else:
160
+ role_str = ""
161
+ role_plain = ""
162
+
163
+ disp_model = model_id
164
+ if len(disp_model) > model_w - 1:
165
+ disp_model = disp_model[:model_w - 4] + '...'
166
+
167
+ line_plain = f" {provider:<{prov_w}}{disp_model:<{model_w}}{role_plain}"
168
+
169
+ if idx == ui.sel:
170
+ # highlighted row
171
+ line_sel = f" > {provider:<{prov_w}}{disp_model:<{model_w}}{role_plain}"
172
+ out.append(wline(row, f"\033[7m{line_sel[:W].ljust(W)}\033[0m"))
173
+ else:
174
+ prov_color = "\033[36m" if provider in LOCAL_PROVIDERS else "\033[35m"
175
+ line_fmt = f" {prov_color}{provider:<{prov_w}}\033[0m{disp_model:<{model_w}}{role_str}"
176
+ out.append(wline(row, line_fmt))
177
+
178
+ if not ui.filtered:
179
+ out.append(wline(body_start, " \033[90mNo models found for this filter.\033[0m"))
180
+
181
+ # -- separator --
182
+ out.append(wline(H - 3, f"\033[90m{'─' * W}\033[0m"))
183
+
184
+ # -- active summary --
185
+ parts = []
186
+ if ui.chat_model:
187
+ parts.append(f"chat: {ui.chat_model}/{ui.chat_provider}")
188
+ if ui.vision_model:
189
+ parts.append(f"vision: {ui.vision_model}/{ui.vision_provider}")
190
+ if ui.embed_model:
191
+ parts.append(f"embed: {ui.embed_model}/{ui.embed_provider}")
192
+ if ui.reasoning_model:
193
+ parts.append(f"reasoning: {ui.reasoning_model}/{ui.reasoning_provider}")
194
+ summary = ' '.join(parts)
195
+ if summary:
196
+ out.append(wline(H - 2, f" \033[33m{summary[:W-2]}\033[0m"))
197
+ else:
198
+ out.append(wline(H - 2, " \033[90mNo active models configured.\033[0m"))
199
+
200
+ # -- status / footer --
201
+ if ui.status:
202
+ stat_line = f" \033[33m{ui.status[:W-2]}\033[0m"
203
+ else:
204
+ stat_line = ""
205
+ # combine status into footer area
206
+ foot = " [Tab] Filter [j/k] Nav [c] Set Chat [v] Set Vision [e] Set Embed [r] Set Reasoning [d] Refresh [q] Quit"
207
+ out.append(wline(H - 1, stat_line))
208
+ out.append(wline(H, f"\033[44;37m{foot[:W].ljust(W)}\033[0m"))
209
+
210
+ sys.stdout.write(''.join(out))
211
+ sys.stdout.flush()
212
+
213
+ # -- input handling ------------------------------------------
214
+ def handle(c):
215
+ if c == '\x1b':
216
+ return handle_esc()
217
+ if c == 'q':
218
+ return False
219
+ elif c == '\t':
220
+ ui.tab = (ui.tab + 1) % len(ui.tabs)
221
+ ui.sel = 0
222
+ ui.scroll = 0
223
+ apply_filter()
224
+ ui.status = ""
225
+ elif c == 'k':
226
+ nav_up()
227
+ elif c == 'j':
228
+ nav_down()
229
+ elif c == 'c':
230
+ assign_role('chat')
231
+ elif c == 'v':
232
+ assign_role('vision')
233
+ elif c == 'e':
234
+ assign_role('embed')
235
+ elif c == 'r':
236
+ assign_role('reasoning')
237
+ elif c == 'd':
238
+ detect_models()
239
+ load_active()
240
+ return True
241
+
242
+ def handle_esc():
243
+ if select.select([sys.stdin], [], [], 0.05)[0]:
244
+ c2 = sys.stdin.read(1)
245
+ if c2 == '[':
246
+ c3 = sys.stdin.read(1)
247
+ if c3 == 'A':
248
+ nav_up()
249
+ elif c3 == 'B':
250
+ nav_down()
251
+ elif c3 == 'Z':
252
+ # Shift+Tab: cycle tabs backward
253
+ ui.tab = (ui.tab - 1) % len(ui.tabs)
254
+ ui.sel = 0
255
+ ui.scroll = 0
256
+ apply_filter()
257
+ ui.status = ""
258
+ # consume any other escape sequence
259
+ else:
260
+ # bare Esc: quit
261
+ return False
262
+ return True
263
+
264
+ def nav_up():
265
+ ui.sel = max(0, ui.sel - 1)
266
+ if ui.sel < ui.scroll:
267
+ ui.scroll = ui.sel
268
+ ui.status = ""
269
+
270
+ def nav_down():
271
+ _, H = term_size()
272
+ body_h = H - 8
273
+ mx = max(0, len(ui.filtered) - 1)
274
+ ui.sel = min(mx, ui.sel + 1)
275
+ if ui.sel >= ui.scroll + body_h:
276
+ ui.scroll = ui.sel - body_h + 1
277
+ ui.status = ""
278
+
279
+ def assign_role(role):
280
+ if not ui.filtered:
281
+ ui.status = "No model selected."
282
+ return
283
+ if ui.sel >= len(ui.filtered):
284
+ ui.status = "No model selected."
285
+ return
286
+ provider, model_id = ui.filtered[ui.sel]
287
+
288
+ if role == 'chat':
289
+ set_npcsh_config_value('NPCSH_CHAT_MODEL', model_id)
290
+ set_npcsh_config_value('NPCSH_CHAT_PROVIDER', provider)
291
+ os.environ['NPCSH_CHAT_MODEL'] = model_id
292
+ os.environ['NPCSH_CHAT_PROVIDER'] = provider
293
+ ui.chat_model = model_id
294
+ ui.chat_provider = provider
295
+ ui.status = f"Chat model set to {model_id}/{provider}"
296
+ elif role == 'vision':
297
+ set_npcsh_config_value('NPCSH_VISION_MODEL', model_id)
298
+ set_npcsh_config_value('NPCSH_VISION_PROVIDER', provider)
299
+ os.environ['NPCSH_VISION_MODEL'] = model_id
300
+ os.environ['NPCSH_VISION_PROVIDER'] = provider
301
+ ui.vision_model = model_id
302
+ ui.vision_provider = provider
303
+ ui.status = f"Vision model set to {model_id}/{provider}"
304
+ elif role == 'embed':
305
+ set_npcsh_config_value('NPCSH_EMBEDDING_MODEL', model_id)
306
+ set_npcsh_config_value('NPCSH_EMBEDDING_PROVIDER', provider)
307
+ os.environ['NPCSH_EMBEDDING_MODEL'] = model_id
308
+ os.environ['NPCSH_EMBEDDING_PROVIDER'] = provider
309
+ ui.embed_model = model_id
310
+ ui.embed_provider = provider
311
+ ui.status = f"Embedding model set to {model_id}/{provider}"
312
+ elif role == 'reasoning':
313
+ set_npcsh_config_value('NPCSH_REASONING_MODEL', model_id)
314
+ set_npcsh_config_value('NPCSH_REASONING_PROVIDER', provider)
315
+ os.environ['NPCSH_REASONING_MODEL'] = model_id
316
+ os.environ['NPCSH_REASONING_PROVIDER'] = provider
317
+ ui.reasoning_model = model_id
318
+ ui.reasoning_provider = provider
319
+ ui.status = f"Reasoning model set to {model_id}/{provider}"
320
+
321
+ # -- main loop -----------------------------------------------
322
+ load_active()
323
+ detect_models()
324
+ fd = sys.stdin.fileno()
325
+ old_attrs = termios.tcgetattr(fd)
326
+
327
+ try:
328
+ tty.setcbreak(fd)
329
+ sys.stdout.write('\033[?25l') # hide cursor
330
+ sys.stdout.write('\033[2J\033[H') # initial full clear
331
+ sys.stdout.flush()
332
+ render()
333
+ while True:
334
+ c = sys.stdin.read(1)
335
+ if not handle(c):
336
+ break
337
+ render()
338
+ finally:
339
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
340
+ sys.stdout.write('\033[?25h\033[2J\033[H')
341
+ sys.stdout.flush()
342
+
343
+ context['output'] = "Models TUI closed."