npcsh 1.1.17__py3-none-any.whl → 1.1.18__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 (169) hide show
  1. npcsh/_state.py +114 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +46 -12
  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 +1 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/execution.py +0 -1
  13. npcsh/guac.py +0 -1
  14. npcsh/mcp_helpers.py +2 -3
  15. npcsh/mcp_server.py +5 -10
  16. npcsh/npc.py +10 -11
  17. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  18. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  19. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  20. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  21. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  22. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  23. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  24. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  25. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  26. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  27. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  28. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  29. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  30. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  31. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  32. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  33. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  34. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  35. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  36. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  37. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  38. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  39. npcsh/npcsh.py +7 -4
  40. npcsh/plonk.py +0 -1
  41. npcsh/pti.py +0 -1
  42. npcsh/routes.py +1 -3
  43. npcsh/spool.py +0 -1
  44. npcsh/ui.py +0 -1
  45. npcsh/wander.py +0 -1
  46. npcsh/yap.py +0 -1
  47. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  48. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  49. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  50. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  51. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  52. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  53. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  54. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  55. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  56. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  57. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  58. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  59. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  60. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  61. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  62. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  63. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  64. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  65. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  66. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  67. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  68. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.jinx +13 -7
  69. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/METADATA +90 -1
  70. npcsh-1.1.18.dist-info/RECORD +235 -0
  71. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  72. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +0 -3
  73. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  74. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  75. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  76. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  77. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  78. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  79. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  80. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  81. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  82. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  83. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  84. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  85. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  86. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  87. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  88. npcsh-1.1.17.dist-info/RECORD +0 -219
  89. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  90. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  91. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  92. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  93. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  94. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +0 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +0 -0
  96. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +0 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  98. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  100. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +0 -0
  102. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +0 -0
  103. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  104. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +0 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  106. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  107. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  108. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  109. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  110. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  112. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  113. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  114. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  115. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +0 -0
  116. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  117. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +0 -0
  118. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  119. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  120. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  121. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  122. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  123. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  124. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  125. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/notify.jinx +0 -0
  126. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  127. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  128. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +0 -0
  129. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  130. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  131. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +0 -0
  132. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  133. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  134. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  135. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/search.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  168. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  169. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -1,20 +1,29 @@
1
1
  jinx_name: mem_search
2
- description: Search memories (approved, pending, or all)
2
+ description: Search memories with interactive TUI and approval workflow
3
3
  inputs:
4
4
  - query: ""
5
5
  - status: "all"
6
6
  - npc_name: ""
7
7
  - team_name: ""
8
- - max_results: "10"
8
+ - max_results: "20"
9
9
  - db_path: ""
10
+ - text: "false"
10
11
 
11
12
  steps:
12
13
  - name: search_memories
13
14
  engine: python
14
15
  code: |
15
16
  import os
17
+ import sys
18
+ import tty
19
+ import termios
20
+ from datetime import datetime
21
+ from npcpy.memory.command_history import CommandHistory
22
+ from npcpy.memory.memory_processor import get_relevant_memories
16
23
 
17
24
  query = context.get('query', '').strip()
25
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
26
+
18
27
  if not query:
19
28
  lines = [
20
29
  "Usage: /mem_search <query> [status=all|approved|pending]",
@@ -23,20 +32,34 @@ steps:
23
32
  " status - Filter by status (all, approved, pending). Default is all",
24
33
  " npc_name - Filter by NPC name",
25
34
  " team_name - Filter by team name",
26
- " max_results - Max results to return (default 10)",
35
+ " max_results - Max results to return (default 20)",
27
36
  " db_path - Path to history database",
37
+ " text - Text-only output, no TUI (true/false)",
38
+ "",
39
+ "TUI Controls:",
40
+ " j/k or arrows - Navigate",
41
+ " 1/2/3 - Sort by time/status/npc",
42
+ " f - Filter by status (all/approved/pending)",
43
+ " p - Preview full memory",
44
+ " a - Approve selected memory",
45
+ " x - Reject selected memory",
46
+ " q/ESC - Quit",
47
+ "",
48
+ "Examples:",
49
+ " /mem_search python",
50
+ " /mem_search debugging status=pending",
28
51
  ]
29
52
  context['output'] = "\n".join(lines)
30
53
  else:
31
54
  status_filter = context.get('status', 'all').lower()
32
- npc_name = context.get('npc_name') or (npc.name if npc else None)
55
+ npc_name = context.get('npc_name') or (npc.name if 'npc' in dir() and npc else None)
33
56
  team_name = context.get('team_name') or None
34
57
  try:
35
58
  team_name = team_name or (state.team.name if 'state' in dir() and state and state.team else None)
36
59
  except:
37
60
  pass
38
- max_results = int(context.get('max_results') or 10)
39
- db_path = context.get('db_path') or os.path.expanduser("~/.npcsh/npcsh_history.db")
61
+ max_results = int(context.get('max_results') or 20)
62
+ db_path = context.get('db_path') or os.path.expanduser("~/npcsh_history.db")
40
63
  current_path = os.getcwd()
41
64
 
42
65
  try:
@@ -54,27 +77,312 @@ steps:
54
77
  state=state_obj
55
78
  )
56
79
  else:
57
- memories = cmd_history.search_memories(
80
+ memories = cmd_history.search_memory(
58
81
  query=query,
59
- npc_name=npc_name,
60
- team_name=team_name,
61
- status=status_filter if status_filter != 'all' else None,
82
+ npc=npc_name,
83
+ team=team_name,
84
+ status_filter=status_filter if status_filter != 'all' else None,
62
85
  limit=max_results
63
86
  )
64
87
 
65
- if not memories:
66
- context['output'] = "No memories found for '" + query + "' (status=" + status_filter + ")"
67
- else:
68
- lines = ["Found " + str(len(memories)) + " memories (status=" + status_filter + "):", ""]
69
- for i, mem in enumerate(memories, 1):
88
+ # Normalize to list of dicts
89
+ if memories:
90
+ normalized = []
91
+ for mem in memories:
70
92
  if isinstance(mem, dict):
71
- ts = mem.get('timestamp', 'unknown')
72
- content = mem.get('final_memory') or mem.get('initial_memory') or mem.get('content', '')
73
- status = mem.get('status', '')
74
- lines.append(str(i) + ". [" + str(ts) + "] (" + status + ") " + str(content))
93
+ normalized.append(mem)
75
94
  else:
76
- lines.append(str(i) + ". " + str(mem))
95
+ normalized.append({'content': str(mem), 'status': 'unknown', 'timestamp': '', 'npc': ''})
96
+ memories = normalized
97
+ else:
98
+ memories = []
99
+
100
+ if not memories:
101
+ context['output'] = f"No memories found for '{query}' (status={status_filter})"
102
+ elif text_mode:
103
+ # Text-only output
104
+ lines = [f"Found {len(memories)} memories (status={status_filter}):", ""]
105
+ for i, mem in enumerate(memories, 1):
106
+ ts = mem.get('timestamp', 'unknown')
107
+ content = mem.get('final_memory') or mem.get('initial_memory') or mem.get('content', '')
108
+ status = mem.get('status', '')
109
+ lines.append(f"{i}. [{ts}] ({status}) {str(content)[:80]}")
77
110
  context['output'] = "\n".join(lines)
111
+ else:
112
+ # Interactive TUI mode
113
+ def get_terminal_size():
114
+ try:
115
+ size = os.get_terminal_size()
116
+ return size.columns, size.lines
117
+ except:
118
+ return 80, 24
119
+
120
+ def format_ts(ts):
121
+ if not ts:
122
+ return 'unknown'
123
+ try:
124
+ if 'T' in str(ts):
125
+ dt = datetime.fromisoformat(str(ts).replace('Z', '+00:00'))
126
+ else:
127
+ dt = datetime.strptime(str(ts)[:19], '%Y-%m-%d %H:%M:%S')
128
+ now = datetime.now()
129
+ diff = now - dt.replace(tzinfo=None)
130
+ if diff.days == 0:
131
+ return f"Today {dt.strftime('%H:%M')}"
132
+ elif diff.days == 1:
133
+ return f"Yesterday {dt.strftime('%H:%M')}"
134
+ elif diff.days < 7:
135
+ return dt.strftime('%a %H:%M')
136
+ else:
137
+ return dt.strftime('%b %d')
138
+ except:
139
+ return str(ts)[:12]
140
+
141
+ width, height = get_terminal_size()
142
+ selected = 0
143
+ scroll = 0
144
+ list_height = height - 5
145
+ mode = 'list'
146
+ preview_scroll = 0
147
+ sort_mode = 'time' # time, status, npc
148
+ current_filter = status_filter
149
+
150
+ def sort_memories(mems, sort_mode):
151
+ if sort_mode == 'time':
152
+ return sorted(mems, key=lambda x: x.get('timestamp') or '', reverse=True)
153
+ elif sort_mode == 'status':
154
+ return sorted(mems, key=lambda x: (x.get('status') or '', x.get('timestamp') or ''), reverse=True)
155
+ elif sort_mode == 'npc':
156
+ return sorted(mems, key=lambda x: (x.get('npc') or '', x.get('timestamp') or ''), reverse=True)
157
+ return mems
158
+
159
+ def filter_memories(mems, filter_status):
160
+ if filter_status == 'all':
161
+ return mems
162
+ return [m for m in mems if m.get('status') == filter_status]
163
+
164
+ display_memories = filter_memories(sort_memories(memories, sort_mode), current_filter)
165
+
166
+ fd = sys.stdin.fileno()
167
+ old_settings = termios.tcgetattr(fd)
168
+
169
+ try:
170
+ tty.setcbreak(fd)
171
+ sys.stdout.write('\033[?25l')
172
+ sys.stdout.write('\033[2J\033[H')
173
+
174
+ while True:
175
+ width, height = get_terminal_size()
176
+ list_height = height - 5
177
+
178
+ if mode == 'list':
179
+ if selected < scroll:
180
+ scroll = selected
181
+ elif selected >= scroll + list_height:
182
+ scroll = selected - list_height + 1
183
+
184
+ sys.stdout.write('\033[H')
185
+
186
+ # Header
187
+ if mode == 'list':
188
+ sort_ind = {'time': '1', 'status': '2', 'npc': '3'}[sort_mode]
189
+ header = f" MEM SEARCH ({len(display_memories)} results): '{query}' [sort:{sort_mode}({sort_ind}) filter:{current_filter}] "
190
+ else:
191
+ header = f" PREVIEW MEMORY "
192
+ sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
193
+
194
+ # Column headers
195
+ if mode == 'list':
196
+ col_header = f' {"STATUS":<10} {"TIMESTAMP":<14} {"NPC":<12} {"CONTENT":<40}'
197
+ sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
198
+ else:
199
+ sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
200
+
201
+ if mode == 'list':
202
+ for i in range(list_height):
203
+ idx = scroll + i
204
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
205
+ if idx >= len(display_memories):
206
+ continue
207
+
208
+ m = display_memories[idx]
209
+ status = (m.get('status') or '?')[:10]
210
+ ts = format_ts(m.get('timestamp'))
211
+ npc_str = (m.get('npc') or 'default')[:12]
212
+ content = (m.get('final_memory') or m.get('initial_memory') or m.get('content', ''))
213
+ content = str(content)[:50].replace('\n', ' ')
214
+
215
+ # Color by status
216
+ if m.get('status') == 'approved':
217
+ status_color = '\033[32m' # green
218
+ elif m.get('status') == 'pending':
219
+ status_color = '\033[33m' # yellow
220
+ elif m.get('status') == 'rejected':
221
+ status_color = '\033[31m' # red
222
+ else:
223
+ status_color = '\033[90m' # gray
224
+
225
+ line = f" {status_color}{status:<10}\033[0m {ts:<14} {npc_str:<12} {content}"
226
+ line = line[:width+15]
227
+
228
+ if idx == selected:
229
+ sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
230
+ else:
231
+ sys.stdout.write(f' {line}')
232
+
233
+ # Status bar
234
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
235
+ sel = display_memories[selected] if display_memories else {}
236
+ mem_id = sel.get('id', '') or sel.get('memory_id', '')
237
+ sys.stdout.write(f'\033[{height-1};1H\033[K ID: {mem_id}'.ljust(width))
238
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav 1/2/3:Sort f:Filter p:Preview a:Approve x:Reject q:Quit [{selected+1}/{len(display_memories)}] \033[0m')
239
+
240
+ else: # preview mode
241
+ sel = display_memories[selected]
242
+ content = sel.get('final_memory') or sel.get('initial_memory') or sel.get('content', '')
243
+ content_lines = str(content).split('\n')
244
+
245
+ # Add metadata at top
246
+ meta_lines = [
247
+ f"Status: {sel.get('status', '')}",
248
+ f"Timestamp: {sel.get('timestamp', '')}",
249
+ f"NPC: {sel.get('npc', '')}",
250
+ f"Team: {sel.get('team', '')}",
251
+ f"ID: {sel.get('id', '') or sel.get('memory_id', '')}",
252
+ "─" * 40,
253
+ ]
254
+ all_lines = meta_lines + content_lines
255
+
256
+ for i in range(list_height):
257
+ idx = preview_scroll + i
258
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
259
+ if idx < len(all_lines):
260
+ sys.stdout.write(all_lines[idx][:width-1])
261
+
262
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
263
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(all_lines)} lines]')
264
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back a:Approve x:Reject q:Quit \033[0m')
265
+
266
+ sys.stdout.flush()
267
+
268
+ c = sys.stdin.read(1)
269
+
270
+ if c == '\x1b':
271
+ c2 = sys.stdin.read(1)
272
+ if c2 == '[':
273
+ c3 = sys.stdin.read(1)
274
+ if c3 == 'A': # Up
275
+ if mode == 'list' and selected > 0:
276
+ selected -= 1
277
+ elif mode == 'preview' and preview_scroll > 0:
278
+ preview_scroll -= 1
279
+ elif c3 == 'B': # Down
280
+ if mode == 'list' and selected < len(display_memories) - 1:
281
+ selected += 1
282
+ elif mode == 'preview':
283
+ sel = display_memories[selected]
284
+ content = str(sel.get('final_memory') or sel.get('content', ''))
285
+ all_lines = content.split('\n')
286
+ if preview_scroll < max(0, len(all_lines) + 6 - list_height):
287
+ preview_scroll += 1
288
+ else:
289
+ if mode == 'preview':
290
+ mode = 'list'
291
+ sys.stdout.write('\033[2J\033[H')
292
+ else:
293
+ context['output'] = "Cancelled."
294
+ break
295
+ continue
296
+
297
+ if c == 'q' or c == '\x03':
298
+ context['output'] = "Cancelled."
299
+ break
300
+ elif c == 'k':
301
+ if mode == 'list' and selected > 0:
302
+ selected -= 1
303
+ elif mode == 'preview' and preview_scroll > 0:
304
+ preview_scroll -= 1
305
+ elif c == 'j':
306
+ if mode == 'list' and selected < len(display_memories) - 1:
307
+ selected += 1
308
+ elif mode == 'preview':
309
+ sel = display_memories[selected]
310
+ content = str(sel.get('final_memory') or sel.get('content', ''))
311
+ all_lines = content.split('\n')
312
+ if preview_scroll < max(0, len(all_lines) + 6 - list_height):
313
+ preview_scroll += 1
314
+ elif c == '1':
315
+ sort_mode = 'time'
316
+ display_memories = filter_memories(sort_memories(memories, sort_mode), current_filter)
317
+ selected = 0
318
+ scroll = 0
319
+ elif c == '2':
320
+ sort_mode = 'status'
321
+ display_memories = filter_memories(sort_memories(memories, sort_mode), current_filter)
322
+ selected = 0
323
+ scroll = 0
324
+ elif c == '3':
325
+ sort_mode = 'npc'
326
+ display_memories = filter_memories(sort_memories(memories, sort_mode), current_filter)
327
+ selected = 0
328
+ scroll = 0
329
+ elif c == 'f' and mode == 'list':
330
+ # Cycle through filters
331
+ if current_filter == 'all':
332
+ current_filter = 'pending'
333
+ elif current_filter == 'pending':
334
+ current_filter = 'approved'
335
+ else:
336
+ current_filter = 'all'
337
+ display_memories = filter_memories(sort_memories(memories, sort_mode), current_filter)
338
+ selected = 0
339
+ scroll = 0
340
+ elif c == 'a' and display_memories:
341
+ # Approve memory
342
+ sel = display_memories[selected]
343
+ mem_id = sel.get('id') or sel.get('memory_id')
344
+ if mem_id:
345
+ try:
346
+ cmd_history.update_memory_status(mem_id, 'approved')
347
+ sel['status'] = 'approved'
348
+ # Update in original list too
349
+ for m in memories:
350
+ if (m.get('id') or m.get('memory_id')) == mem_id:
351
+ m['status'] = 'approved'
352
+ except Exception as e:
353
+ pass
354
+ elif c == 'x' and display_memories:
355
+ # Reject memory
356
+ sel = display_memories[selected]
357
+ mem_id = sel.get('id') or sel.get('memory_id')
358
+ if mem_id:
359
+ try:
360
+ cmd_history.update_memory_status(mem_id, 'rejected')
361
+ sel['status'] = 'rejected'
362
+ for m in memories:
363
+ if (m.get('id') or m.get('memory_id')) == mem_id:
364
+ m['status'] = 'rejected'
365
+ except Exception as e:
366
+ pass
367
+ elif c == 'p' and mode == 'list' and display_memories:
368
+ mode = 'preview'
369
+ preview_scroll = 0
370
+ sys.stdout.write('\033[2J\033[H')
371
+ elif c == 'b' and mode == 'preview':
372
+ mode = 'list'
373
+ sys.stdout.write('\033[2J\033[H')
374
+ elif c in ('\r', '\n') and display_memories:
375
+ sel = display_memories[selected]
376
+ content = sel.get('final_memory') or sel.get('content', '')
377
+ context['output'] = f"Selected memory:\n\n{content}"
378
+ break
379
+
380
+ finally:
381
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
382
+ sys.stdout.write('\033[?25h')
383
+ sys.stdout.write('\033[2J\033[H')
384
+ sys.stdout.flush()
385
+
78
386
  except Exception as e:
79
387
  import traceback
80
388
  context['output'] = "Memory search error: " + str(e) + "\n" + traceback.format_exc()