npcsh 1.1.16__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 (217) hide show
  1. npcsh/_state.py +138 -100
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +28 -0
  4. npcsh/benchmark/npcsh_agent.py +296 -0
  5. npcsh/benchmark/runner.py +611 -0
  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 +146 -0
  18. npcsh/npc_team/jinxs/bin/nql.jinx +7 -7
  19. npcsh/npc_team/jinxs/bin/roll.jinx +20 -23
  20. npcsh/npc_team/jinxs/bin/sample.jinx +6 -7
  21. npcsh/npc_team/jinxs/bin/sync.jinx +6 -6
  22. npcsh/npc_team/jinxs/bin/vixynt.jinx +8 -8
  23. npcsh/npc_team/jinxs/incognide/add_tab.jinx +11 -0
  24. npcsh/npc_team/jinxs/incognide/close_pane.jinx +9 -0
  25. npcsh/npc_team/jinxs/incognide/close_tab.jinx +10 -0
  26. npcsh/npc_team/jinxs/incognide/confirm.jinx +10 -0
  27. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +9 -0
  28. npcsh/npc_team/jinxs/{npc_studio/npc-studio.jinx → incognide/incognide.jinx} +2 -2
  29. npcsh/npc_team/jinxs/incognide/list_panes.jinx +8 -0
  30. npcsh/npc_team/jinxs/incognide/navigate.jinx +10 -0
  31. npcsh/npc_team/jinxs/incognide/notify.jinx +10 -0
  32. npcsh/npc_team/jinxs/incognide/open_pane.jinx +13 -0
  33. npcsh/npc_team/jinxs/incognide/read_pane.jinx +9 -0
  34. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +10 -0
  35. npcsh/npc_team/jinxs/incognide/send_message.jinx +10 -0
  36. npcsh/npc_team/jinxs/incognide/split_pane.jinx +12 -0
  37. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +10 -0
  38. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +10 -0
  39. npcsh/npc_team/jinxs/incognide/write_file.jinx +11 -0
  40. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +9 -0
  41. npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +4 -4
  42. npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +1 -1
  43. npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +2 -2
  44. npcsh/npc_team/jinxs/lib/computer_use/click.jinx +2 -2
  45. npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +1 -1
  46. npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +1 -1
  47. npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +1 -1
  48. npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +2 -2
  49. npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +1 -1
  50. npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +1 -1
  51. npcsh/npc_team/jinxs/lib/core/chat.jinx +4 -4
  52. npcsh/npc_team/jinxs/lib/core/cmd.jinx +4 -4
  53. npcsh/npc_team/jinxs/lib/core/compress.jinx +8 -8
  54. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +3 -0
  55. npcsh/npc_team/jinxs/lib/core/ots.jinx +7 -7
  56. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +348 -0
  57. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +339 -0
  58. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +418 -0
  59. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  60. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +388 -0
  61. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +283 -0
  62. npcsh/npc_team/jinxs/lib/core/search.jinx +52 -129
  63. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  64. npcsh/npc_team/jinxs/lib/core/sleep.jinx +29 -18
  65. npcsh/npc_team/jinxs/lib/core/sql.jinx +15 -11
  66. npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +7 -7
  67. npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +8 -9
  68. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +389 -78
  69. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +373 -56
  70. npcsh/npc_team/jinxs/lib/utils/build.jinx +5 -5
  71. npcsh/npc_team/jinxs/lib/utils/compile.jinx +2 -2
  72. npcsh/npc_team/jinxs/lib/utils/help.jinx +1 -1
  73. npcsh/npc_team/jinxs/lib/utils/init.jinx +5 -5
  74. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +300 -145
  75. npcsh/npc_team/jinxs/lib/utils/serve.jinx +2 -2
  76. npcsh/npc_team/jinxs/lib/utils/set.jinx +2 -2
  77. npcsh/npc_team/jinxs/lib/utils/switch.jinx +3 -3
  78. npcsh/npc_team/jinxs/lib/utils/switches.jinx +1 -1
  79. npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +2 -2
  80. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  81. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  82. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  83. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  84. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  85. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  86. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  87. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  88. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  89. {npcsh-1.1.16.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/yap.jinx +8 -2
  90. npcsh/npc_team/sibiji.npc +1 -1
  91. npcsh/npcsh.py +87 -46
  92. npcsh/plonk.py +0 -1
  93. npcsh/pti.py +0 -1
  94. npcsh/routes.py +1 -3
  95. npcsh/spool.py +0 -1
  96. npcsh/ui.py +0 -1
  97. npcsh/wander.py +0 -1
  98. npcsh/yap.py +0 -1
  99. npcsh-1.1.18.data/data/npcsh/npc_team/add_tab.jinx +11 -0
  100. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  101. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  102. npcsh-1.1.18.data/data/npcsh/npc_team/benchmark.jinx +146 -0
  103. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +4 -4
  104. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +1 -1
  105. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +5 -5
  106. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +4 -4
  107. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +2 -2
  108. npcsh-1.1.18.data/data/npcsh/npc_team/close_pane.jinx +9 -0
  109. npcsh-1.1.18.data/data/npcsh/npc_team/close_tab.jinx +10 -0
  110. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +4 -4
  111. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +2 -2
  112. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +8 -8
  113. npcsh-1.1.18.data/data/npcsh/npc_team/confirm.jinx +10 -0
  114. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +7 -7
  115. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  116. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  117. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +8 -9
  118. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +3 -0
  119. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  120. npcsh-1.1.18.data/data/npcsh/npc_team/focus_pane.jinx +9 -0
  121. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  122. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +1 -1
  123. npcsh-1.1.16.data/data/npcsh/npc_team/npc-studio.jinx → npcsh-1.1.18.data/data/npcsh/npc_team/incognide.jinx +2 -2
  124. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +5 -5
  125. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  126. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +1 -1
  127. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  128. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +1 -1
  129. npcsh-1.1.18.data/data/npcsh/npc_team/list_panes.jinx +8 -0
  130. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  131. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  132. npcsh-1.1.18.data/data/npcsh/npc_team/navigate.jinx +10 -0
  133. npcsh-1.1.18.data/data/npcsh/npc_team/notify.jinx +10 -0
  134. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +7 -7
  135. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +2 -2
  136. npcsh-1.1.18.data/data/npcsh/npc_team/open_pane.jinx +13 -0
  137. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +7 -7
  138. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  139. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  140. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  141. npcsh-1.1.18.data/data/npcsh/npc_team/read_pane.jinx +9 -0
  142. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  143. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +20 -23
  144. npcsh-1.1.18.data/data/npcsh/npc_team/run_terminal.jinx +10 -0
  145. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +6 -7
  146. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +1 -1
  147. npcsh-1.1.18.data/data/npcsh/npc_team/search.jinx +54 -0
  148. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  149. npcsh-1.1.18.data/data/npcsh/npc_team/send_message.jinx +10 -0
  150. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +2 -2
  151. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +2 -2
  152. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +1 -1
  153. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +1 -1
  154. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +29 -18
  155. npcsh-1.1.18.data/data/npcsh/npc_team/split_pane.jinx +12 -0
  156. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  157. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  158. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +3 -3
  159. npcsh-1.1.18.data/data/npcsh/npc_team/switch_npc.jinx +10 -0
  160. npcsh-1.1.18.data/data/npcsh/npc_team/switch_tab.jinx +10 -0
  161. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +1 -1
  162. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +6 -6
  163. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +2 -2
  164. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +2 -2
  165. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +1 -1
  166. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +8 -8
  167. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +1 -1
  168. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  169. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  170. npcsh-1.1.18.data/data/npcsh/npc_team/write_file.jinx +11 -0
  171. {npcsh/npc_team/jinxs/bin → npcsh-1.1.18.data/data/npcsh/npc_team}/yap.jinx +8 -2
  172. npcsh-1.1.18.data/data/npcsh/npc_team/zen_mode.jinx +9 -0
  173. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/METADATA +99 -7
  174. npcsh-1.1.18.dist-info/RECORD +235 -0
  175. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  176. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +2 -3
  177. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  178. npcsh/npc_team/jinxs/bin/wander.jinx +0 -152
  179. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  180. npcsh-1.1.16.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  181. npcsh-1.1.16.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  182. npcsh-1.1.16.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  183. npcsh-1.1.16.data/data/npcsh/npc_team/search.jinx +0 -131
  184. npcsh-1.1.16.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  185. npcsh-1.1.16.data/data/npcsh/npc_team/spool.jinx +0 -161
  186. npcsh-1.1.16.data/data/npcsh/npc_team/sql.jinx +0 -16
  187. npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +0 -152
  188. npcsh-1.1.16.dist-info/RECORD +0 -170
  189. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  190. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  191. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  192. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  193. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  194. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  195. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  196. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  197. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  198. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  199. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  200. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  201. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  202. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  203. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  204. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  205. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  206. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  207. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  208. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  209. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  210. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  211. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  212. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  213. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  214. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  215. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  216. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  217. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,388 @@
1
+ jinx_name: mem_search
2
+ description: Search memories with interactive TUI and approval workflow
3
+ inputs:
4
+ - query: ""
5
+ - status: "all"
6
+ - npc_name: ""
7
+ - team_name: ""
8
+ - max_results: "20"
9
+ - db_path: ""
10
+ - text: "false"
11
+
12
+ steps:
13
+ - name: search_memories
14
+ engine: python
15
+ code: |
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
23
+
24
+ query = context.get('query', '').strip()
25
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
26
+
27
+ if not query:
28
+ lines = [
29
+ "Usage: /mem_search <query> [status=all|approved|pending]",
30
+ "",
31
+ "Options:",
32
+ " status - Filter by status (all, approved, pending). Default is all",
33
+ " npc_name - Filter by NPC name",
34
+ " team_name - Filter by team name",
35
+ " max_results - Max results to return (default 20)",
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",
51
+ ]
52
+ context['output'] = "\n".join(lines)
53
+ else:
54
+ status_filter = context.get('status', 'all').lower()
55
+ npc_name = context.get('npc_name') or (npc.name if 'npc' in dir() and npc else None)
56
+ team_name = context.get('team_name') or None
57
+ try:
58
+ team_name = team_name or (state.team.name if 'state' in dir() and state and state.team else None)
59
+ except:
60
+ pass
61
+ max_results = int(context.get('max_results') or 20)
62
+ db_path = context.get('db_path') or os.path.expanduser("~/npcsh_history.db")
63
+ current_path = os.getcwd()
64
+
65
+ try:
66
+ cmd_history = CommandHistory(db_path)
67
+
68
+ if status_filter == 'approved':
69
+ state_obj = state if 'state' in dir() else None
70
+ memories = get_relevant_memories(
71
+ command_history=cmd_history,
72
+ npc_name=npc_name or '__none__',
73
+ team_name=team_name or '__none__',
74
+ path=current_path,
75
+ query=query,
76
+ max_memories=max_results,
77
+ state=state_obj
78
+ )
79
+ else:
80
+ memories = cmd_history.search_memory(
81
+ query=query,
82
+ npc=npc_name,
83
+ team=team_name,
84
+ status_filter=status_filter if status_filter != 'all' else None,
85
+ limit=max_results
86
+ )
87
+
88
+ # Normalize to list of dicts
89
+ if memories:
90
+ normalized = []
91
+ for mem in memories:
92
+ if isinstance(mem, dict):
93
+ normalized.append(mem)
94
+ else:
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]}")
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
+
386
+ except Exception as e:
387
+ import traceback
388
+ context['output'] = "Memory search error: " + str(e) + "\n" + traceback.format_exc()
@@ -0,0 +1,283 @@
1
+ jinx_name: web_search
2
+ description: Search the web with interactive TUI
3
+ inputs:
4
+ - query: ""
5
+ - provider: ""
6
+ - num_results: "10"
7
+ - text: "false"
8
+
9
+ steps:
10
+ - name: search_web
11
+ engine: python
12
+ code: |
13
+ import os
14
+ import sys
15
+ import tty
16
+ import termios
17
+ import webbrowser
18
+ import subprocess
19
+ from npcpy.data.web import search_web
20
+
21
+ query = context.get('query', '').strip()
22
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
23
+
24
+ if not query:
25
+ lines = [
26
+ "Usage: /web_search <query>",
27
+ "",
28
+ "Options:",
29
+ " provider - Search provider (default uses state.search_provider)",
30
+ " num_results - Number of results (default 10)",
31
+ " text - Text-only output, no TUI (true/false)",
32
+ "",
33
+ "TUI Controls:",
34
+ " j/k or arrows - Navigate",
35
+ " p - Preview page content",
36
+ " o - Open in browser",
37
+ " i - Open in incognide",
38
+ " c - Copy URL to clipboard",
39
+ " q/ESC - Quit",
40
+ "",
41
+ "Examples:",
42
+ " /web_search python asyncio tutorial",
43
+ " /web_search react hooks num_results=20",
44
+ ]
45
+ context['output'] = "\n".join(lines)
46
+ else:
47
+ provider = context.get('provider') or None
48
+ try:
49
+ provider = provider or (state.search_provider if 'state' in dir() and state else None)
50
+ except:
51
+ pass
52
+ num_results = int(context.get('num_results') or 10)
53
+
54
+ try:
55
+ raw_results = search_web(query, provider=provider, num_results=num_results)
56
+
57
+ # Normalize results
58
+ results = []
59
+ if raw_results:
60
+ for res in raw_results:
61
+ if isinstance(res, dict):
62
+ results.append({
63
+ 'title': res.get('title', 'No title'),
64
+ 'url': res.get('url', res.get('link', '')),
65
+ 'snippet': res.get('snippet', res.get('description', ''))
66
+ })
67
+ else:
68
+ results.append({'title': str(res)[:50], 'url': str(res), 'snippet': ''})
69
+
70
+ if not results:
71
+ context['output'] = "No web results found."
72
+ elif text_mode:
73
+ # Text-only output
74
+ lines = []
75
+ for res in results:
76
+ lines.append(f"- {res['title']}")
77
+ lines.append(f" {res['url']}")
78
+ if res.get('snippet'):
79
+ lines.append(f" {res['snippet'][:100]}...")
80
+ lines.append("")
81
+ context['output'] = "\n".join(lines)
82
+ else:
83
+ # Interactive TUI mode
84
+ def get_terminal_size():
85
+ try:
86
+ size = os.get_terminal_size()
87
+ return size.columns, size.lines
88
+ except:
89
+ return 80, 24
90
+
91
+ def get_domain(url):
92
+ try:
93
+ from urllib.parse import urlparse
94
+ return urlparse(url).netloc[:25]
95
+ except:
96
+ return url[:25]
97
+
98
+ width, height = get_terminal_size()
99
+ selected = 0
100
+ scroll = 0
101
+ list_height = height - 5
102
+ mode = 'list'
103
+ preview_scroll = 0
104
+ preview_content = []
105
+
106
+ fd = sys.stdin.fileno()
107
+ old_settings = termios.tcgetattr(fd)
108
+
109
+ try:
110
+ tty.setcbreak(fd)
111
+ sys.stdout.write('\033[?25l')
112
+ sys.stdout.write('\033[2J\033[H')
113
+
114
+ while True:
115
+ width, height = get_terminal_size()
116
+ list_height = height - 5
117
+
118
+ if mode == 'list':
119
+ if selected < scroll:
120
+ scroll = selected
121
+ elif selected >= scroll + list_height:
122
+ scroll = selected - list_height + 1
123
+
124
+ sys.stdout.write('\033[H')
125
+
126
+ # Header
127
+ if mode == 'list':
128
+ header = f" WEB SEARCH ({len(results)} results): '{query}' "
129
+ else:
130
+ header = f" PREVIEW: {results[selected]['title'][:width-12]} "
131
+ sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
132
+
133
+ # Column headers
134
+ if mode == 'list':
135
+ col_header = f' {"#":<3} {"DOMAIN":<25} {"TITLE":<50}'
136
+ sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
137
+ else:
138
+ sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
139
+
140
+ if mode == 'list':
141
+ for i in range(list_height):
142
+ idx = scroll + i
143
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
144
+ if idx >= len(results):
145
+ continue
146
+
147
+ r = results[idx]
148
+ num = str(idx + 1)
149
+ domain = get_domain(r['url'])
150
+ title = r['title'][:55].replace('\n', ' ')
151
+
152
+ line = f" {num:<3} {domain:<25} {title}"
153
+ line = line[:width-1]
154
+
155
+ if idx == selected:
156
+ sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
157
+ else:
158
+ sys.stdout.write(f' {line}')
159
+
160
+ # Status bar
161
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
162
+ sel = results[selected] if results else {}
163
+ snippet = (sel.get('snippet') or '')[:width-2]
164
+ sys.stdout.write(f'\033[{height-1};1H\033[K {snippet}'.ljust(width))
165
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav p:Preview o:Open i:Incog c:Copy q:Quit [{selected+1}/{len(results)}] \033[0m')
166
+
167
+ else: # preview mode
168
+ for i in range(list_height):
169
+ idx = preview_scroll + i
170
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
171
+ if idx < len(preview_content):
172
+ sys.stdout.write(preview_content[idx][:width-1])
173
+
174
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
175
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(preview_content)} lines]')
176
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back o:Open c:Copy q:Quit \033[0m')
177
+
178
+ sys.stdout.flush()
179
+
180
+ c = sys.stdin.read(1)
181
+
182
+ if c == '\x1b':
183
+ c2 = sys.stdin.read(1)
184
+ if c2 == '[':
185
+ c3 = sys.stdin.read(1)
186
+ if c3 == 'A':
187
+ if mode == 'list' and selected > 0:
188
+ selected -= 1
189
+ elif mode == 'preview' and preview_scroll > 0:
190
+ preview_scroll -= 1
191
+ elif c3 == 'B':
192
+ if mode == 'list' and selected < len(results) - 1:
193
+ selected += 1
194
+ elif mode == 'preview' and preview_scroll < max(0, len(preview_content) - list_height):
195
+ preview_scroll += 1
196
+ else:
197
+ if mode == 'preview':
198
+ mode = 'list'
199
+ sys.stdout.write('\033[2J\033[H')
200
+ else:
201
+ context['output'] = "Cancelled."
202
+ break
203
+ continue
204
+
205
+ if c == 'q' or c == '\x03':
206
+ context['output'] = "Cancelled."
207
+ break
208
+ elif c == 'k':
209
+ if mode == 'list' and selected > 0:
210
+ selected -= 1
211
+ elif mode == 'preview' and preview_scroll > 0:
212
+ preview_scroll -= 1
213
+ elif c == 'j':
214
+ if mode == 'list' and selected < len(results) - 1:
215
+ selected += 1
216
+ elif mode == 'preview' and preview_scroll < max(0, len(preview_content) - list_height):
217
+ preview_scroll += 1
218
+ elif c == 'p' and mode == 'list' and results:
219
+ # Build preview from available info
220
+ sel = results[selected]
221
+ preview_content = [
222
+ f"Title: {sel['title']}",
223
+ "",
224
+ f"URL: {sel['url']}",
225
+ f"Domain: {get_domain(sel['url'])}",
226
+ "",
227
+ ]
228
+ if sel.get('snippet'):
229
+ preview_content.append("Snippet:")
230
+ # Word wrap snippet
231
+ words = sel['snippet'].split()
232
+ line = ""
233
+ for w in words:
234
+ if len(line) + len(w) + 1 > width - 4:
235
+ preview_content.append(f" {line}")
236
+ line = w
237
+ else:
238
+ line = f"{line} {w}" if line else w
239
+ if line:
240
+ preview_content.append(f" {line}")
241
+
242
+ mode = 'preview'
243
+ preview_scroll = 0
244
+ sys.stdout.write('\033[2J\033[H')
245
+ elif c == 'b' and mode == 'preview':
246
+ mode = 'list'
247
+ sys.stdout.write('\033[2J\033[H')
248
+ elif c == 'o' and results:
249
+ url = results[selected].get('url', '')
250
+ if url:
251
+ webbrowser.open(url)
252
+ elif c == 'i' and results:
253
+ url = results[selected].get('url', '')
254
+ if url:
255
+ try:
256
+ subprocess.run(['npcsh', '-c', f'/navigate url={url}'], check=False, capture_output=True)
257
+ except:
258
+ webbrowser.open(url)
259
+ elif c == 'c' and results:
260
+ url = results[selected].get('url', '')
261
+ if url:
262
+ try:
263
+ # Try xclip or xsel
264
+ subprocess.run(['xclip', '-selection', 'clipboard'], input=url.encode(), check=True)
265
+ except:
266
+ try:
267
+ subprocess.run(['xsel', '--clipboard', '--input'], input=url.encode(), check=True)
268
+ except:
269
+ pass
270
+ elif c in ('\r', '\n') and results:
271
+ sel = results[selected]
272
+ context['output'] = f"Selected: {sel['title']}\nURL: {sel['url']}"
273
+ break
274
+
275
+ finally:
276
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
277
+ sys.stdout.write('\033[?25h')
278
+ sys.stdout.write('\033[2J\033[H')
279
+ sys.stdout.flush()
280
+
281
+ except Exception as e:
282
+ import traceback
283
+ context['output'] = "Web search error: " + str(e) + "\n" + traceback.format_exc()