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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. npcsh/_state.py +16 -78
  2. npcsh/diff_viewer.py +3 -3
  3. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  4. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +18 -7
  5. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +18 -7
  6. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +20 -9
  7. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +53 -15
  8. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  9. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +393 -317
  10. npcsh/npc_team/jinxs/lib/utils/models.jinx +343 -0
  11. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +8 -7
  12. npcsh/npc_team/jinxs/modes/alicanto.jinx +1573 -296
  13. npcsh/npc_team/jinxs/modes/arxiv.jinx +6 -6
  14. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  15. npcsh/npc_team/jinxs/modes/corca.jinx +4 -4
  16. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  17. npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
  18. npcsh/npc_team/jinxs/modes/kg.jinx +941 -0
  19. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  20. npcsh/npc_team/jinxs/modes/nql.jinx +460 -0
  21. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  22. npcsh/npc_team/jinxs/modes/plonk.jinx +490 -304
  23. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  24. npcsh/npc_team/jinxs/modes/reattach.jinx +4 -4
  25. npcsh/npc_team/jinxs/modes/spool.jinx +4 -4
  26. npcsh/npc_team/jinxs/modes/team.jinx +504 -0
  27. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  28. npcsh/npc_team/jinxs/modes/wander.jinx +455 -182
  29. npcsh/npc_team/jinxs/modes/yap.jinx +10 -3
  30. npcsh/npcsh.py +112 -47
  31. npcsh/routes.py +12 -3
  32. npcsh/salmon_simulation.py +0 -0
  33. npcsh-1.1.21.data/data/npcsh/npc_team/alicanto.jinx +1633 -0
  34. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/arxiv.jinx +6 -6
  35. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  36. npcsh-1.1.21.data/data/npcsh/npc_team/compress.jinx +428 -0
  37. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  38. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.jinx +4 -4
  39. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/db_search.jinx +18 -7
  40. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/file_search.jinx +18 -7
  41. npcsh-1.1.21.data/data/npcsh/npc_team/git.jinx +795 -0
  42. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.jinx +4 -4
  43. npcsh-1.1.21.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  44. npcsh-1.1.21.data/data/npcsh/npc_team/kg.jinx +941 -0
  45. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kg_search.jinx +20 -9
  46. npcsh-1.1.21.data/data/npcsh/npc_team/memories.jinx +414 -0
  47. npcsh-1.1.21.data/data/npcsh/npc_team/models.jinx +343 -0
  48. npcsh-1.1.21.data/data/npcsh/npc_team/nql.jinx +460 -0
  49. npcsh-1.1.21.data/data/npcsh/npc_team/papers.jinx +578 -0
  50. npcsh-1.1.21.data/data/npcsh/npc_team/plonk.jinx +565 -0
  51. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/pti.jinx +1 -1
  52. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/reattach.jinx +4 -4
  53. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/setup.jinx +8 -7
  54. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.jinx +4 -4
  55. npcsh-1.1.21.data/data/npcsh/npc_team/team.jinx +504 -0
  56. npcsh-1.1.21.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  57. npcsh-1.1.21.data/data/npcsh/npc_team/wander.jinx +728 -0
  58. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/web_search.jinx +53 -15
  59. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.jinx +10 -3
  60. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/METADATA +2 -2
  61. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/RECORD +147 -148
  62. npcsh-1.1.21.dist-info/entry_points.txt +11 -0
  63. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -299
  64. npcsh/npc_team/jinxs/bin/memories.jinx +0 -316
  65. npcsh/npc_team/jinxs/bin/nql.jinx +0 -141
  66. npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
  67. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  68. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  69. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  70. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  71. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  72. npcsh/npc_team/plonkjr.npc +0 -23
  73. npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  74. npcsh-1.1.19.data/data/npcsh/npc_team/compress.jinx +0 -140
  75. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +0 -299
  76. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  77. npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  78. npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  79. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +0 -316
  80. npcsh-1.1.19.data/data/npcsh/npc_team/nql.jinx +0 -141
  81. npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  82. npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +0 -379
  83. npcsh-1.1.19.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  84. npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  85. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +0 -327
  86. npcsh-1.1.19.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  87. npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +0 -455
  88. npcsh-1.1.19.dist-info/entry_points.txt +0 -22
  89. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  90. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  91. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  92. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  93. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  94. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  95. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  96. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.png +0 -0
  97. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  98. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  99. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/build.jinx +0 -0
  100. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/chat.jinx +0 -0
  101. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/click.jinx +0 -0
  102. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  103. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  104. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  105. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  106. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/compile.jinx +0 -0
  107. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  108. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/convene.jinx +0 -0
  109. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.npc +0 -0
  110. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.png +0 -0
  111. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca_example.png +0 -0
  112. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  113. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  114. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  115. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic.npc +0 -0
  116. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic4.png +0 -0
  117. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.npc +0 -0
  118. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.png +0 -0
  119. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/help.jinx +0 -0
  120. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  121. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/init.jinx +0 -0
  122. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  123. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  124. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  125. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  126. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  127. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  128. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  129. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/notify.jinx +0 -0
  130. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  131. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  132. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  133. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  134. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/ots.jinx +0 -0
  135. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/paste.jinx +0 -0
  136. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.npc +0 -0
  137. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.png +0 -0
  138. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  139. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/python.jinx +0 -0
  140. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  141. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/roll.jinx +0 -0
  142. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  143. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sample.jinx +0 -0
  144. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  145. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/search.jinx +0 -0
  146. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  147. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/serve.jinx +0 -0
  148. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/set.jinx +0 -0
  149. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sh.jinx +0 -0
  150. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/shh.jinx +0 -0
  151. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  152. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.png +0 -0
  153. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  154. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  155. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.png +0 -0
  156. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sql.jinx +0 -0
  157. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch.jinx +0 -0
  158. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  159. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  160. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switches.jinx +0 -0
  161. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sync.jinx +0 -0
  162. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  163. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  164. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  165. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/usage.jinx +0 -0
  166. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  167. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/wait.jinx +0 -0
  168. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  169. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.png +0 -0
  170. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  171. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/WHEEL +0 -0
  172. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/licenses/LICENSE +0 -0
  173. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,414 @@
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
+ import time
16
+ from datetime import datetime
17
+
18
+ if not sys.stdin.isatty():
19
+ context['output'] = "Memory browser requires an interactive terminal."
20
+
21
+ else:
22
+ from npcpy.memory.command_history import CommandHistory
23
+ from npcsh.config import NPCSH_DB_PATH
24
+ from sqlalchemy import text
25
+
26
+ db_path = os.path.expanduser(NPCSH_DB_PATH)
27
+ command_history = CommandHistory(db_path)
28
+
29
+ # ========== Discover actual status values from DB ==========
30
+ db_statuses = []
31
+ try:
32
+ with command_history.engine.connect() as conn:
33
+ rows = conn.execute(text("SELECT DISTINCT status FROM memory_lifecycle ORDER BY status"))
34
+ db_statuses = [r[0] for r in rows if r[0]]
35
+ except:
36
+ pass
37
+
38
+ # Build tabs: All + pending first, then rest
39
+ ordered = []
40
+ for s in db_statuses:
41
+ if 'pending' in s.lower():
42
+ ordered.insert(0, s)
43
+ else:
44
+ ordered.append(s)
45
+ tab_names = ['All'] + ordered
46
+ tab_filters = [None] + ordered
47
+
48
+ # ========== State ==========
49
+ class MemState:
50
+ tab = 0
51
+ tabs = tab_names
52
+ tab_filt = tab_filters
53
+ memories = []
54
+ sel = 0
55
+ scroll = 0
56
+ preview = False
57
+ msg = ""
58
+ msg_color = "33"
59
+ npc_filter = None
60
+ team_filter = None
61
+ approved_count = 0
62
+ rejected_count = 0
63
+
64
+ st = MemState()
65
+
66
+ # ========== Helpers ==========
67
+ def get_size():
68
+ try:
69
+ s = os.get_terminal_size()
70
+ return s.columns, s.lines
71
+ except:
72
+ return 80, 24
73
+
74
+ def status_icon(sv):
75
+ if not sv:
76
+ return '\033[90m?\033[0m'
77
+ s = sv.lower()
78
+ if 'approved' in s:
79
+ return '\033[1;32m+\033[0m'
80
+ if 'edited' in s:
81
+ return '\033[1;36m~\033[0m'
82
+ if 'rejected' in s:
83
+ return '\033[1;31m-\033[0m'
84
+ if 'pending' in s:
85
+ return '\033[1;33m*\033[0m'
86
+ return '\033[90m?\033[0m'
87
+
88
+ def status_color(sv):
89
+ if not sv:
90
+ return '0'
91
+ s = sv.lower()
92
+ if 'approved' in s:
93
+ return '32'
94
+ if 'edited' in s:
95
+ return '36'
96
+ if 'rejected' in s:
97
+ return '31'
98
+ if 'pending' in s:
99
+ return '33'
100
+ return '0'
101
+
102
+ def load_memories():
103
+ st.memories = []
104
+ try:
105
+ with command_history.engine.connect() as conn:
106
+ sf = st.tab_filt[st.tab] if st.tab < len(st.tab_filt) else None
107
+ q = "SELECT id, created_at, npc, team, directory_path, initial_memory, final_memory, status FROM memory_lifecycle"
108
+ conds = []
109
+ params = {}
110
+
111
+ if sf:
112
+ conds.append("status = :sf")
113
+ params['sf'] = sf
114
+ if st.npc_filter:
115
+ conds.append("npc = :npc")
116
+ params['npc'] = st.npc_filter
117
+ if st.team_filter:
118
+ conds.append("team = :team")
119
+ params['team'] = st.team_filter
120
+
121
+ if conds:
122
+ q += " WHERE " + " AND ".join(conds)
123
+ q += " ORDER BY created_at DESC LIMIT 200"
124
+
125
+ result = conn.execute(text(q), params)
126
+ for row in result:
127
+ st.memories.append({
128
+ 'id': row[0],
129
+ 'created_at': row[1],
130
+ 'npc': row[2],
131
+ 'team': row[3],
132
+ 'scope': row[4] or '',
133
+ 'original': row[5],
134
+ 'final': row[6],
135
+ 'status': row[7]
136
+ })
137
+ except Exception as e:
138
+ st.msg = "DB Error: " + str(e)
139
+ st.msg_color = "31"
140
+
141
+ def do_update(memory_id, new_status, final_mem=None):
142
+ """Update memory status in DB and reload list."""
143
+ try:
144
+ command_history.update_memory_status(memory_id, new_status, final_mem)
145
+ old_count = len(st.memories)
146
+ old_sel = st.sel
147
+ load_memories()
148
+ new_count = len(st.memories)
149
+
150
+ # Clamp selection
151
+ if st.memories:
152
+ st.sel = min(old_sel, len(st.memories) - 1)
153
+ else:
154
+ st.sel = 0
155
+
156
+ # Fix scroll
157
+ _, height = get_size()
158
+ vis = max(1, height - 7)
159
+ if st.sel < st.scroll:
160
+ st.scroll = st.sel
161
+ elif st.sel >= st.scroll + vis:
162
+ st.scroll = st.sel - vis + 1
163
+
164
+ return True
165
+ except Exception as e:
166
+ st.msg = "UPDATE FAILED: " + str(e)
167
+ st.msg_color = "31"
168
+ return False
169
+
170
+ def format_date(dt_str):
171
+ if not dt_str:
172
+ return " "
173
+ try:
174
+ if isinstance(dt_str, str):
175
+ dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
176
+ else:
177
+ dt = dt_str
178
+ return dt.strftime('%m-%d %H:%M')
179
+ except:
180
+ return str(dt_str)[:10]
181
+
182
+ # ========== Rendering ==========
183
+ def render():
184
+ width, height = get_size()
185
+ out = []
186
+
187
+ # Header with session stats
188
+ stats = ""
189
+ if st.approved_count or st.rejected_count:
190
+ stats = " [+" + str(st.approved_count) + " -" + str(st.rejected_count) + "]"
191
+ header = " MEMORIES (" + str(len(st.memories)) + ")" + stats + " "
192
+ out.append("\033[1;1H\033[K\033[7;1m" + header.ljust(width) + "\033[0m")
193
+
194
+ # Tabs
195
+ tab_str = ""
196
+ for i, tab in enumerate(st.tabs):
197
+ if i == st.tab:
198
+ tab_str += "\033[1;7m [" + tab + "] \033[0m"
199
+ else:
200
+ tab_str += " \033[90m" + tab + "\033[0m "
201
+ out.append("\033[2;1H\033[K " + tab_str)
202
+
203
+ # Separator
204
+ out.append("\033[3;1H\033[K\033[90m" + ("-" * width) + "\033[0m")
205
+
206
+ if st.preview and st.memories:
207
+ render_preview(out, width, height)
208
+ else:
209
+ render_list(out, width, height)
210
+
211
+ # Status bar
212
+ out.append("\033[" + str(height-2) + ";1H\033[K\033[90m" + ("-" * width) + "\033[0m")
213
+ out.append("\033[" + str(height-1) + ";1H\033[K")
214
+ if st.msg:
215
+ out.append(" \033[" + st.msg_color + ";1m" + st.msg[:width-2] + "\033[0m")
216
+
217
+ # Footer
218
+ if st.preview:
219
+ foot = " [Esc] Back [a] Approve [x] Reject [j/k] Prev/Next [q] Quit "
220
+ else:
221
+ foot = " [Tab] Filter [j/k] Nav [Enter] Preview [a] Approve [x] Reject [q] Quit "
222
+ out.append("\033[" + str(height) + ";1H\033[K\033[7m" + foot.ljust(width) + "\033[0m")
223
+
224
+ sys.stdout.write(''.join(out))
225
+ sys.stdout.flush()
226
+
227
+ def render_list(out, width, height):
228
+ vis_h = height - 7
229
+ if vis_h < 1:
230
+ vis_h = 1
231
+
232
+ for i in range(vis_h):
233
+ row = 4 + i
234
+ out.append("\033[" + str(row) + ";1H\033[K")
235
+ idx = st.scroll + i
236
+ if idx >= len(st.memories):
237
+ continue
238
+
239
+ mem = st.memories[idx]
240
+ icon = status_icon(mem['status'])
241
+ date_str = format_date(mem['created_at'])
242
+ npc_str = (mem['npc'] or '-')[:8]
243
+ content = (mem['final'] or mem['original'] or '')[:width-35].replace('\n', ' ')
244
+
245
+ line = icon + " " + date_str + " " + npc_str.ljust(9) + content
246
+
247
+ if idx == st.sel:
248
+ out.append("\033[7m " + line + " \033[0m")
249
+ else:
250
+ out.append(" " + line)
251
+
252
+ # Scroll indicator
253
+ if len(st.memories) > vis_h and vis_h > 0:
254
+ total = max(1, len(st.memories) - vis_h)
255
+ pct = int((st.scroll / total) * 100) if total > 0 else 0
256
+ out.append("\033[4;" + str(width-6) + "H\033[90m[" + str(pct) + "%]\033[0m")
257
+
258
+ if not st.memories:
259
+ out.append("\033[6;4H\033[90mNo memories found for this filter.\033[0m")
260
+ out.append("\033[7;4H\033[90mTry switching tabs with Tab key.\033[0m")
261
+
262
+ def render_preview(out, width, height):
263
+ if not st.memories or st.sel >= len(st.memories):
264
+ return
265
+
266
+ mem = st.memories[st.sel]
267
+
268
+ row = 4
269
+ sc = status_color(mem['status'])
270
+ out.append("\033[" + str(row) + ";1H\033[K\033[1m Memory #" + str(mem['id']) + " \033[" + sc + "m[" + str(mem['status']) + "]\033[0m")
271
+ row += 1
272
+
273
+ out.append("\033[" + str(row) + ";1H\033[K\033[90m Date: " + format_date(mem['created_at']) + " NPC: " + str(mem['npc'] or '-') + " Team: " + str(mem['team'] or '-') + "\033[0m")
274
+ row += 1
275
+ out.append("\033[" + str(row) + ";1H\033[K\033[90m Scope: " + str(mem['scope'] or '-')[:60] + "\033[0m")
276
+ row += 1
277
+ out.append("\033[" + str(row) + ";1H\033[K")
278
+ row += 1
279
+
280
+ out.append("\033[" + str(row) + ";1H\033[K\033[1m Content:\033[0m")
281
+ row += 1
282
+
283
+ content = mem['final'] or mem['original'] or '(empty)'
284
+ for line in content.split('\n')[:height - row - 3]:
285
+ out.append("\033[" + str(row) + ";1H\033[K " + line[:width-5])
286
+ row += 1
287
+
288
+ if mem['final'] and mem['original'] and mem['final'] != mem['original']:
289
+ out.append("\033[" + str(row) + ";1H\033[K")
290
+ row += 1
291
+ out.append("\033[" + str(row) + ";1H\033[K\033[90;1m Original:\033[0m")
292
+ row += 1
293
+ for line in mem['original'].split('\n')[:3]:
294
+ if row >= height - 3:
295
+ break
296
+ out.append("\033[" + str(row) + ";1H\033[K\033[90m " + line[:width-5] + "\033[0m")
297
+ row += 1
298
+
299
+ while row < height - 2:
300
+ out.append("\033[" + str(row) + ";1H\033[K")
301
+ row += 1
302
+
303
+ # ========== Input ==========
304
+ def handle_input(c):
305
+ if c == 'q' or c == '\x03':
306
+ return False
307
+
308
+ if c == '\x1b':
309
+ if select.select([fd], [], [], 0.05)[0]:
310
+ c2 = os.read(fd, 1).decode('latin-1')
311
+ if c2 == '[':
312
+ c3 = os.read(fd, 1).decode('latin-1')
313
+ if c3 == 'A':
314
+ move_up()
315
+ elif c3 == 'B':
316
+ move_down()
317
+ elif c3 == 'Z':
318
+ # Shift+Tab = prev tab
319
+ st.tab = (st.tab - 1) % len(st.tabs)
320
+ st.sel = 0
321
+ st.scroll = 0
322
+ load_memories()
323
+ st.msg = ""
324
+ else:
325
+ if st.preview:
326
+ st.preview = False
327
+ return True
328
+
329
+ if c == '\t':
330
+ st.tab = (st.tab + 1) % len(st.tabs)
331
+ st.sel = 0
332
+ st.scroll = 0
333
+ load_memories()
334
+ st.msg = ""
335
+ st.msg_color = "33"
336
+ elif c == 'k':
337
+ move_up()
338
+ elif c == 'j':
339
+ move_down()
340
+ elif c == '\r' or c == '\n' or c == 'p':
341
+ if st.memories:
342
+ st.preview = not st.preview
343
+ elif c == 'a':
344
+ approve_current()
345
+ elif c == 'x':
346
+ reject_current()
347
+
348
+ return True
349
+
350
+ def move_up():
351
+ st.sel = max(0, st.sel - 1)
352
+ if st.sel < st.scroll:
353
+ st.scroll = st.sel
354
+ st.msg = ""
355
+
356
+ def move_down():
357
+ _, height = get_size()
358
+ vis = max(1, height - 7)
359
+ st.sel = min(max(0, len(st.memories) - 1), st.sel + 1)
360
+ if st.sel >= st.scroll + vis:
361
+ st.scroll = st.sel - vis + 1
362
+ st.msg = ""
363
+
364
+ def approve_current():
365
+ if not st.memories or st.sel >= len(st.memories):
366
+ st.msg = "No memory selected"
367
+ st.msg_color = "33"
368
+ return
369
+ mem = st.memories[st.sel]
370
+ final = mem.get('final') or mem.get('original')
371
+ if do_update(mem['id'], 'human-approved', final):
372
+ st.approved_count += 1
373
+ st.msg = "APPROVED #" + str(mem['id'])
374
+ st.msg_color = "32"
375
+
376
+ def reject_current():
377
+ if not st.memories or st.sel >= len(st.memories):
378
+ st.msg = "No memory selected"
379
+ st.msg_color = "33"
380
+ return
381
+ mem = st.memories[st.sel]
382
+ if do_update(mem['id'], 'human-rejected'):
383
+ st.rejected_count += 1
384
+ st.msg = "REJECTED #" + str(mem['id'])
385
+ st.msg_color = "31"
386
+
387
+ # ========== Main Loop ==========
388
+ load_memories()
389
+
390
+ fd = sys.stdin.fileno()
391
+ old_settings = termios.tcgetattr(fd)
392
+
393
+ try:
394
+ tty.setcbreak(fd)
395
+ sys.stdout.write('\033[?25l')
396
+ sys.stdout.write('\033[2J')
397
+ render()
398
+
399
+ while True:
400
+ c = os.read(fd, 1).decode('latin-1')
401
+ if not handle_input(c):
402
+ break
403
+ render()
404
+
405
+ finally:
406
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
407
+ sys.stdout.write('\033[?25h')
408
+ sys.stdout.write('\033[2J\033[H')
409
+ sys.stdout.flush()
410
+
411
+ summary = "Memory browser closed."
412
+ if st.approved_count or st.rejected_count:
413
+ summary += " Session: " + str(st.approved_count) + " approved, " + str(st.rejected_count) + " rejected."
414
+ context['output'] = summary