npcsh 1.1.17__py3-none-any.whl → 1.1.19__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. npcsh/_state.py +122 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +87 -22
  5. npcsh/benchmark/runner.py +85 -43
  6. npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
  7. npcsh/build.py +2 -4
  8. npcsh/completion.py +2 -6
  9. npcsh/config.py +2 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/diff_viewer.py +452 -0
  13. npcsh/execution.py +0 -1
  14. npcsh/guac.py +0 -1
  15. npcsh/mcp_helpers.py +2 -3
  16. npcsh/mcp_server.py +5 -10
  17. npcsh/npc.py +10 -11
  18. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  19. npcsh/npc_team/jinxs/bin/config_tui.jinx +299 -0
  20. npcsh/npc_team/jinxs/bin/memories.jinx +316 -0
  21. npcsh/npc_team/jinxs/bin/setup.jinx +240 -0
  22. npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
  23. npcsh/npc_team/jinxs/bin/team_tui.jinx +327 -0
  24. npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
  25. npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
  26. npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
  27. npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
  28. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
  29. npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
  30. npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
  31. npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
  32. npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
  33. npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
  34. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
  35. npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
  36. npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
  37. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
  38. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
  39. npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
  40. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
  41. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  42. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  43. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  44. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  45. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  46. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  47. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  48. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  49. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  50. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  51. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  52. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  53. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  54. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  55. npcsh/npc_team/jinxs/modes/guac.jinx +542 -0
  56. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  57. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  58. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  59. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  60. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  61. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  62. npcsh/npcsh.py +7 -4
  63. npcsh/plonk.py +0 -1
  64. npcsh/pti.py +0 -1
  65. npcsh/routes.py +1 -3
  66. npcsh/spool.py +0 -1
  67. npcsh/ui.py +0 -1
  68. npcsh/wander.py +0 -1
  69. npcsh/yap.py +0 -1
  70. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
  71. npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  72. npcsh-1.1.19.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  73. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  74. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
  75. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
  76. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +299 -0
  77. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
  78. npcsh-1.1.19.data/data/npcsh/npc_team/corca.jinx +430 -0
  79. npcsh-1.1.19.data/data/npcsh/npc_team/db_search.jinx +348 -0
  80. npcsh-1.1.19.data/data/npcsh/npc_team/file_search.jinx +339 -0
  81. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
  82. npcsh-1.1.19.data/data/npcsh/npc_team/guac.jinx +542 -0
  83. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  84. npcsh-1.1.19.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  85. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
  86. npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  87. npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  88. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
  89. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
  90. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
  91. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
  92. npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  93. npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +379 -0
  94. npcsh-1.1.19.data/data/npcsh/npc_team/pti.jinx +357 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
  96. npcsh-1.1.19.data/data/npcsh/npc_team/reattach.jinx +291 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
  98. npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/send_message.jinx +1 -1
  100. npcsh-1.1.19.data/data/npcsh/npc_team/setup.jinx +240 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  102. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
  103. npcsh-1.1.19.data/data/npcsh/npc_team/spool.jinx +350 -0
  104. npcsh-1.1.19.data/data/npcsh/npc_team/sql.jinx +20 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
  106. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
  107. npcsh-1.1.19.data/data/npcsh/npc_team/sync.jinx +223 -0
  108. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +327 -0
  109. npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +455 -0
  110. npcsh-1.1.19.data/data/npcsh/npc_team/web_search.jinx +283 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
  112. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +13 -7
  113. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
  114. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/METADATA +110 -14
  115. npcsh-1.1.19.dist-info/RECORD +244 -0
  116. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/WHEEL +1 -1
  117. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -3
  118. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  119. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  120. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  121. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  122. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  123. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  124. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  125. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  126. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  127. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  128. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  129. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  130. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  131. npcsh-1.1.17.data/data/npcsh/npc_team/sync.jinx +0 -230
  132. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  133. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  134. npcsh-1.1.17.dist-info/RECORD +0 -219
  135. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  168. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
  169. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
  170. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
  171. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
  172. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  173. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  174. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
  175. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
  176. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
  177. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  178. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
  179. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
  180. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
  181. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
  182. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
  183. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  184. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
  185. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
  186. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
  187. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
  188. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  189. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  190. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  191. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
  192. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  193. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  194. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
  195. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
  196. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
  197. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,316 @@
1
+ jinx_name: memories
2
+ description: Interactive TUI for browsing and managing npcsh memories
3
+ inputs:
4
+ - scope: ""
5
+ steps:
6
+ - name: memory_browser
7
+ engine: python
8
+ code: |
9
+ import os
10
+ import sys
11
+ import tty
12
+ import termios
13
+ import select
14
+ from datetime import datetime
15
+
16
+ if not sys.stdin.isatty():
17
+ context['output'] = "Memory browser requires an interactive terminal."
18
+ return
19
+
20
+ from npcpy.memory.command_history import CommandHistory
21
+ from npcsh.config import NPCSH_DB_PATH
22
+
23
+ db_path = os.path.expanduser(NPCSH_DB_PATH)
24
+ command_history = CommandHistory(db_path)
25
+
26
+ # ========== State ==========
27
+ class MemoryState:
28
+ def __init__(self):
29
+ self.tab = 0 # 0=All, 1=Pending, 2=Approved, 3=Rejected
30
+ self.tabs = ['All', 'Pending', 'Approved', 'Rejected']
31
+ self.memories = []
32
+ self.selected_idx = 0
33
+ self.scroll_offset = 0
34
+ self.preview_mode = False
35
+ self.status = ""
36
+ self.filters = {
37
+ 'npc': None,
38
+ 'team': None,
39
+ }
40
+
41
+ state = MemoryState()
42
+
43
+ # ========== Helpers ==========
44
+ def get_size():
45
+ try:
46
+ s = os.get_terminal_size()
47
+ return s.columns, s.lines
48
+ except:
49
+ return 80, 24
50
+
51
+ def load_memories():
52
+ """Load memories based on current tab filter."""
53
+ state.memories = []
54
+
55
+ try:
56
+ with command_history.engine.connect() as conn:
57
+ status_filter = {
58
+ 0: None, # All
59
+ 1: 'pending',
60
+ 2: 'approved',
61
+ 3: 'rejected'
62
+ }.get(state.tab)
63
+
64
+ query = "SELECT id, created_at, npc_name, team_name, scope, original_memory, final_memory, status FROM memories"
65
+ conditions = []
66
+
67
+ if status_filter:
68
+ conditions.append(f"status = '{status_filter}'")
69
+ if state.filters['npc']:
70
+ conditions.append(f"npc_name = '{state.filters['npc']}'")
71
+ if state.filters['team']:
72
+ conditions.append(f"team_name = '{state.filters['team']}'")
73
+
74
+ if conditions:
75
+ query += " WHERE " + " AND ".join(conditions)
76
+
77
+ query += " ORDER BY created_at DESC LIMIT 100"
78
+
79
+ from sqlalchemy import text
80
+ result = conn.execute(text(query))
81
+ for row in result:
82
+ state.memories.append({
83
+ 'id': row[0],
84
+ 'created_at': row[1],
85
+ 'npc': row[2],
86
+ 'team': row[3],
87
+ 'scope': row[4],
88
+ 'original': row[5],
89
+ 'final': row[6],
90
+ 'status': row[7]
91
+ })
92
+ except Exception as e:
93
+ state.status = f"Error loading memories: {e}"
94
+
95
+ def update_memory_status(memory_id, new_status):
96
+ """Update a memory's status."""
97
+ try:
98
+ command_history.update_memory_status(memory_id, new_status)
99
+ state.status = f"Memory {memory_id} marked as {new_status}"
100
+ load_memories()
101
+ except Exception as e:
102
+ state.status = f"Error: {e}"
103
+
104
+ def format_date(dt_str):
105
+ """Format datetime string for display."""
106
+ if not dt_str:
107
+ return ""
108
+ try:
109
+ if isinstance(dt_str, str):
110
+ dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
111
+ else:
112
+ dt = dt_str
113
+ return dt.strftime('%m-%d %H:%M')
114
+ except:
115
+ return str(dt_str)[:10]
116
+
117
+ # ========== Rendering ==========
118
+ def render_screen():
119
+ width, height = get_size()
120
+ out = []
121
+ out.append("\033[2J\033[H")
122
+
123
+ # Header
124
+ header = " Memory Browser "
125
+ out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
126
+ out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;1m{header}\033[0m")
127
+
128
+ # Tabs
129
+ tab_str = ""
130
+ for i, tab in enumerate(state.tabs):
131
+ count = sum(1 for m in state.memories if state.tab == 0 or True) # Will filter properly
132
+ if i == state.tab:
133
+ tab_str += f"\033[47;30m [{tab}] \033[0m"
134
+ else:
135
+ tab_str += f" [{tab}] "
136
+ out.append(f"\033[2;2H{tab_str}")
137
+
138
+ # Separator
139
+ out.append(f"\033[3;1H\033[90m{'─' * width}\033[0m")
140
+
141
+ if state.preview_mode and state.memories:
142
+ render_preview(out, width, height)
143
+ else:
144
+ render_list(out, width, height)
145
+
146
+ # Status
147
+ if state.status:
148
+ out.append(f"\033[{height-2};2H\033[33m{state.status[:width-4]}\033[0m")
149
+
150
+ # Footer
151
+ if state.preview_mode:
152
+ footer = "[Esc] Back [a] Approve [x] Reject [j/k] Prev/Next"
153
+ else:
154
+ footer = "[Tab] Filter [j/k] Navigate [p] Preview [a] Approve [x] Reject [q] Quit"
155
+ out.append(f"\033[{height};1H\033[90m{footer[:width]}\033[0m")
156
+
157
+ sys.stdout.write(''.join(out))
158
+ sys.stdout.flush()
159
+
160
+ def render_list(out, width, height):
161
+ """Render memory list."""
162
+ visible_height = height - 7
163
+ visible = state.memories[state.scroll_offset:state.scroll_offset + visible_height]
164
+
165
+ if not state.memories:
166
+ out.append(f"\033[5;4H\033[90mNo memories found.\033[0m")
167
+ return
168
+
169
+ row = 4
170
+ for i, mem in enumerate(visible):
171
+ idx = i + state.scroll_offset
172
+
173
+ # Status indicator
174
+ status_icon = {
175
+ 'pending': '\033[33m○\033[0m',
176
+ 'approved': '\033[32m●\033[0m',
177
+ 'rejected': '\033[31m✗\033[0m'
178
+ }.get(mem['status'], '?')
179
+
180
+ # Format line
181
+ date_str = format_date(mem['created_at'])
182
+ npc_str = mem['npc'][:8] if mem['npc'] else '-'
183
+ content = (mem['final'] or mem['original'] or '')[:width-35]
184
+ content = content.replace('\n', ' ')
185
+
186
+ if idx == state.selected_idx:
187
+ out.append(f"\033[{row};2H\033[47;30m{status_icon} {date_str} {npc_str:<8} {content}\033[0m")
188
+ else:
189
+ out.append(f"\033[{row};2H{status_icon} {date_str} \033[90m{npc_str:<8}\033[0m {content}")
190
+
191
+ row += 1
192
+
193
+ # Scroll indicator
194
+ if len(state.memories) > visible_height:
195
+ pct = int((state.scroll_offset / max(1, len(state.memories) - visible_height)) * 100)
196
+ out.append(f"\033[4;{width-6}H\033[90m[{pct}%]\033[0m")
197
+
198
+ def render_preview(out, width, height):
199
+ """Render memory preview."""
200
+ if not state.memories or state.selected_idx >= len(state.memories):
201
+ return
202
+
203
+ mem = state.memories[state.selected_idx]
204
+
205
+ row = 4
206
+ out.append(f"\033[{row};2H\033[1mMemory #{mem['id']}\033[0m")
207
+ row += 1
208
+
209
+ # Metadata
210
+ status_color = {'pending': '33', 'approved': '32', 'rejected': '31'}.get(mem['status'], '0')
211
+ out.append(f"\033[{row};2HStatus: \033[{status_color}m{mem['status']}\033[0m")
212
+ row += 1
213
+ out.append(f"\033[{row};2HDate: {format_date(mem['created_at'])}")
214
+ row += 1
215
+ out.append(f"\033[{row};2HNPC: {mem['npc'] or '-'} Team: {mem['team'] or '-'} Scope: {mem['scope'] or '-'}")
216
+ row += 2
217
+
218
+ # Content
219
+ out.append(f"\033[{row};2H\033[1mContent:\033[0m")
220
+ row += 1
221
+
222
+ content = mem['final'] or mem['original'] or '(empty)'
223
+ content_lines = content.split('\n')
224
+ for line in content_lines[:height-row-3]:
225
+ out.append(f"\033[{row};4H{line[:width-6]}")
226
+ row += 1
227
+
228
+ # ========== Input Handling ==========
229
+ def handle_input(c):
230
+ if c == 'q':
231
+ return False
232
+
233
+ if c == '\x1b': # Escape
234
+ if select.select([sys.stdin], [], [], 0.05)[0]:
235
+ c2 = sys.stdin.read(1)
236
+ if c2 == '[':
237
+ c3 = sys.stdin.read(1)
238
+ if c3 == 'A': # Up
239
+ move_up()
240
+ elif c3 == 'B': # Down
241
+ move_down()
242
+ else:
243
+ if state.preview_mode:
244
+ state.preview_mode = False
245
+ return True
246
+
247
+ if c == '\t': # Tab - cycle tabs
248
+ state.tab = (state.tab + 1) % len(state.tabs)
249
+ state.selected_idx = 0
250
+ state.scroll_offset = 0
251
+ load_memories()
252
+ state.status = ""
253
+
254
+ elif c == 'k':
255
+ move_up()
256
+ elif c == 'j':
257
+ move_down()
258
+ elif c == 'p' or c == '\r' or c == '\n':
259
+ if state.memories:
260
+ state.preview_mode = not state.preview_mode
261
+ elif c == 'a':
262
+ approve_current()
263
+ elif c == 'x':
264
+ reject_current()
265
+
266
+ return True
267
+
268
+ def move_up():
269
+ state.selected_idx = max(0, state.selected_idx - 1)
270
+ if state.selected_idx < state.scroll_offset:
271
+ state.scroll_offset = state.selected_idx
272
+ state.status = ""
273
+
274
+ def move_down():
275
+ _, height = get_size()
276
+ visible_height = height - 7
277
+ state.selected_idx = min(len(state.memories) - 1, state.selected_idx + 1)
278
+ if state.selected_idx >= state.scroll_offset + visible_height:
279
+ state.scroll_offset = state.selected_idx - visible_height + 1
280
+ state.status = ""
281
+
282
+ def approve_current():
283
+ if state.memories and state.selected_idx < len(state.memories):
284
+ mem = state.memories[state.selected_idx]
285
+ update_memory_status(mem['id'], 'approved')
286
+
287
+ def reject_current():
288
+ if state.memories and state.selected_idx < len(state.memories):
289
+ mem = state.memories[state.selected_idx]
290
+ update_memory_status(mem['id'], 'rejected')
291
+
292
+ # ========== Main Loop ==========
293
+ load_memories()
294
+
295
+ fd = sys.stdin.fileno()
296
+ old_settings = termios.tcgetattr(fd)
297
+
298
+ try:
299
+ tty.setcbreak(fd)
300
+ sys.stdout.write('\033[?25l') # Hide cursor
301
+
302
+ render_screen()
303
+
304
+ while True:
305
+ c = sys.stdin.read(1)
306
+ if not handle_input(c):
307
+ break
308
+ render_screen()
309
+
310
+ finally:
311
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
312
+ sys.stdout.write('\033[?25h') # Show cursor
313
+ sys.stdout.write('\033[2J\033[H') # Clear screen
314
+ sys.stdout.flush()
315
+
316
+ context['output'] = "Memory browser closed."
@@ -1,4 +1,4 @@
1
- jinx_name: studio.navigate
1
+ jinx_name: studio_navigate
2
2
  description: Navigate a browser pane to a specific URL.
3
3
  inputs:
4
4
  - paneId: "active"
@@ -1,4 +1,4 @@
1
- jinx_name: studio.notify
1
+ jinx_name: studio_notify
2
2
  description: Show a notification toast in NPC Studio.
3
3
  inputs:
4
4
  - message: ""
@@ -1,4 +1,4 @@
1
- jinx_name: studio.open_pane
1
+ jinx_name: studio_open_pane
2
2
  description: Open a new pane in NPC Studio. Supports editor, terminal, browser, pdf, csv, chat, image, folder, and other content types.
3
3
  inputs:
4
4
  - type: ""