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,418 @@
1
+ jinx_name: kg_search
2
+ description: Search the knowledge graph with interactive TUI
3
+ inputs:
4
+ - query: ""
5
+ - type: "facts"
6
+ - mode: "keyword"
7
+ - concept: ""
8
+ - depth: "2"
9
+ - breadth: "5"
10
+ - max_results: "20"
11
+ - threshold: "0.6"
12
+ - npc_name: ""
13
+ - team_name: ""
14
+ - db_path: ""
15
+ - text: "false"
16
+
17
+ steps:
18
+ - name: search_kg
19
+ engine: python
20
+ code: |
21
+ import os
22
+ import sys
23
+ import tty
24
+ import termios
25
+ from npcpy.memory.command_history import CommandHistory
26
+ from npcpy.memory.knowledge_graph import (
27
+ kg_search_facts, kg_list_concepts, kg_get_facts_for_concept,
28
+ kg_get_all_facts, kg_link_search, kg_embedding_search,
29
+ kg_hybrid_search, kg_explore_concept
30
+ )
31
+
32
+ query = context.get('query', '').strip()
33
+ search_type = context.get('type', 'facts').lower()
34
+ search_mode = context.get('mode', 'keyword').lower()
35
+ concept = context.get('concept', '').strip()
36
+ max_depth = int(context.get('depth') or 2)
37
+ breadth = int(context.get('breadth') or 5)
38
+ max_results = int(context.get('max_results') or 20)
39
+ threshold = float(context.get('threshold') or 0.6)
40
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
41
+
42
+ if not query and search_type == 'facts' and not concept:
43
+ lines = [
44
+ "Usage: /kg_search <query> [mode=keyword|embedding|link|hybrid|all]",
45
+ "",
46
+ "Search Modes:",
47
+ " keyword - Simple keyword matching (default, fast)",
48
+ " embedding - Semantic similarity using embeddings",
49
+ " link - Traverse graph links from keyword matches",
50
+ " hybrid - Combine keyword + link traversal",
51
+ " all - Combine all methods (slowest, most thorough)",
52
+ "",
53
+ "Options:",
54
+ " type - Result type: facts, concepts, all",
55
+ " concept - Explore a specific concept",
56
+ " depth - Link traversal depth (default: 2)",
57
+ " breadth - Results per traversal hop (default: 5)",
58
+ " max_results - Max total results (default: 20)",
59
+ " threshold - Embedding similarity threshold (default: 0.6)",
60
+ " text - Text-only output, no TUI (true/false)",
61
+ "",
62
+ "TUI Controls:",
63
+ " j/k or arrows - Navigate",
64
+ " 1/2/3 - Sort by score/concept/type",
65
+ " c - Toggle concepts view",
66
+ " e - Explore selected concept",
67
+ " p - Preview full fact/concept",
68
+ " q/ESC - Quit",
69
+ "",
70
+ "Examples:",
71
+ " /kg_search python",
72
+ " /kg_search python mode=embedding",
73
+ " /kg_search python mode=link depth=3",
74
+ " /kg_search type=concepts",
75
+ " /kg_search concept=coding",
76
+ ]
77
+ context['output'] = "\n".join(lines)
78
+ else:
79
+ db_path = context.get('db_path') or os.path.expanduser("~/npcsh_history.db")
80
+
81
+ try:
82
+ cmd_history = CommandHistory(db_path)
83
+ engine = cmd_history.engine
84
+
85
+ team_obj = None
86
+ try:
87
+ team_obj = state.team if 'state' in dir() and state else None
88
+ except:
89
+ pass
90
+ npc_obj = npc if 'npc' in dir() else None
91
+
92
+ emodel = None
93
+ eprovider = None
94
+ try:
95
+ if 'state' in dir() and state:
96
+ emodel = getattr(state, 'embedding_model', None)
97
+ eprovider = getattr(state, 'embedding_provider', None)
98
+ except:
99
+ pass
100
+
101
+ # Collect results
102
+ results = []
103
+
104
+ if concept:
105
+ result = kg_explore_concept(engine, concept, max_depth=max_depth, breadth_per_step=breadth)
106
+ for i, f in enumerate(result.get('direct_facts', [])):
107
+ results.append({'type': 'fact', 'content': str(f), 'score': 1.0, 'concept': concept, 'source': 'direct'})
108
+ for i, f in enumerate(result.get('extended_facts', [])):
109
+ results.append({'type': 'fact', 'content': str(f), 'score': 0.5, 'concept': concept, 'source': 'extended'})
110
+ for c in result.get('related_concepts', []):
111
+ results.append({'type': 'concept', 'content': str(c), 'score': 0.8, 'concept': concept, 'source': 'related'})
112
+
113
+ elif search_type == 'concepts':
114
+ concepts = kg_list_concepts(engine, search_all_scopes=True)
115
+ for c in concepts:
116
+ results.append({'type': 'concept', 'content': str(c), 'score': 1.0, 'concept': str(c), 'source': 'list'})
117
+
118
+ elif search_type == 'all' and not query:
119
+ facts = kg_get_all_facts(engine, search_all_scopes=True)
120
+ for f in facts[:max_results]:
121
+ results.append({'type': 'fact', 'content': str(f), 'score': 1.0, 'concept': '', 'source': 'all'})
122
+
123
+ elif search_mode == 'embedding':
124
+ raw_results = kg_embedding_search(
125
+ engine, query,
126
+ embedding_model=emodel, embedding_provider=eprovider,
127
+ similarity_threshold=threshold, max_results=max_results,
128
+ search_all_scopes=True
129
+ )
130
+ for r in raw_results:
131
+ results.append({
132
+ 'type': r.get('type', 'fact'),
133
+ 'content': str(r.get('content', '')),
134
+ 'score': r.get('score', 0),
135
+ 'concept': r.get('concept', ''),
136
+ 'source': 'embedding'
137
+ })
138
+
139
+ elif search_mode == 'link':
140
+ raw_results = kg_link_search(
141
+ engine, query,
142
+ max_depth=max_depth, breadth_per_step=breadth, max_results=max_results,
143
+ search_all_scopes=True
144
+ )
145
+ for r in raw_results:
146
+ results.append({
147
+ 'type': r.get('type', 'fact'),
148
+ 'content': str(r.get('content', '')),
149
+ 'score': r.get('score', 0),
150
+ 'concept': r.get('concept', ''),
151
+ 'source': f"link-d{r.get('depth', 0)}"
152
+ })
153
+
154
+ elif search_mode in ['hybrid', 'all', 'keyword+link', 'keyword+embedding']:
155
+ raw_results = kg_hybrid_search(
156
+ engine, query,
157
+ mode=search_mode if search_mode != 'hybrid' else 'keyword+link',
158
+ max_depth=max_depth, breadth_per_step=breadth, max_results=max_results,
159
+ embedding_model=emodel, embedding_provider=eprovider,
160
+ similarity_threshold=threshold,
161
+ search_all_scopes=True
162
+ )
163
+ for r in raw_results:
164
+ results.append({
165
+ 'type': r.get('type', 'fact'),
166
+ 'content': str(r.get('content', '')),
167
+ 'score': r.get('score', 0),
168
+ 'concept': r.get('concept', ''),
169
+ 'source': r.get('source', 'hybrid')
170
+ })
171
+
172
+ else:
173
+ # Default keyword search
174
+ facts = kg_search_facts(engine, query, search_all_scopes=True)
175
+ for f in facts[:max_results]:
176
+ results.append({'type': 'fact', 'content': str(f), 'score': 1.0, 'concept': '', 'source': 'keyword'})
177
+
178
+ if not results:
179
+ context['output'] = f"No KG results found for '{query}'"
180
+ elif text_mode:
181
+ # Text-only output
182
+ lines = [f"Found {len(results)} KG results:", ""]
183
+ for i, r in enumerate(results, 1):
184
+ score = f"{r['score']:.2f}" if isinstance(r['score'], float) else str(r['score'])
185
+ lines.append(f"{i}. [{r['type']} {score}] {r['content'][:80]}")
186
+ context['output'] = "\n".join(lines)
187
+ else:
188
+ # Interactive TUI mode
189
+ def get_terminal_size():
190
+ try:
191
+ size = os.get_terminal_size()
192
+ return size.columns, size.lines
193
+ except:
194
+ return 80, 24
195
+
196
+ width, height = get_terminal_size()
197
+ selected = 0
198
+ scroll = 0
199
+ list_height = height - 5
200
+ mode = 'list'
201
+ preview_scroll = 0
202
+ sort_mode = 'score' # score, concept, type
203
+ type_filter = 'all' # all, fact, concept
204
+
205
+ def sort_results(results, sort_mode):
206
+ if sort_mode == 'score':
207
+ return sorted(results, key=lambda x: x.get('score', 0), reverse=True)
208
+ elif sort_mode == 'concept':
209
+ return sorted(results, key=lambda x: (x.get('concept', ''), -x.get('score', 0)))
210
+ elif sort_mode == 'type':
211
+ return sorted(results, key=lambda x: (x.get('type', ''), -x.get('score', 0)))
212
+ return results
213
+
214
+ def filter_results(results, type_filter):
215
+ if type_filter == 'all':
216
+ return results
217
+ return [r for r in results if r.get('type') == type_filter]
218
+
219
+ display_results = filter_results(sort_results(results, sort_mode), type_filter)
220
+
221
+ fd = sys.stdin.fileno()
222
+ old_settings = termios.tcgetattr(fd)
223
+
224
+ try:
225
+ tty.setcbreak(fd)
226
+ sys.stdout.write('\033[?25l')
227
+ sys.stdout.write('\033[2J\033[H')
228
+
229
+ while True:
230
+ width, height = get_terminal_size()
231
+ list_height = height - 5
232
+
233
+ if mode == 'list':
234
+ if selected < scroll:
235
+ scroll = selected
236
+ elif selected >= scroll + list_height:
237
+ scroll = selected - list_height + 1
238
+
239
+ sys.stdout.write('\033[H')
240
+
241
+ # Header
242
+ if mode == 'list':
243
+ sort_ind = {'score': '1', 'concept': '2', 'type': '3'}[sort_mode]
244
+ q = query or concept or search_type
245
+ header = f" KG SEARCH ({len(display_results)} results): '{q}' [sort:{sort_mode}({sort_ind}) filter:{type_filter}] "
246
+ else:
247
+ header = f" PREVIEW: {display_results[selected]['type']} "
248
+ sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
249
+
250
+ # Column headers
251
+ if mode == 'list':
252
+ col_header = f' {"TYPE":<8} {"SCORE":<6} {"CONCEPT":<15} {"CONTENT":<50}'
253
+ sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
254
+ else:
255
+ sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
256
+
257
+ if mode == 'list':
258
+ for i in range(list_height):
259
+ idx = scroll + i
260
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
261
+ if idx >= len(display_results):
262
+ continue
263
+
264
+ r = display_results[idx]
265
+ rtype = r.get('type', '?')[:8]
266
+ score = f"{r.get('score', 0):.2f}" if isinstance(r.get('score'), float) else str(r.get('score', ''))[:6]
267
+ concept_str = (r.get('concept', '') or '')[:15]
268
+ content = (r.get('content', '') or '')[:60].replace('\n', ' ')
269
+
270
+ # Color by type
271
+ if r.get('type') == 'concept':
272
+ type_color = '\033[35m' # magenta
273
+ else:
274
+ type_color = '\033[36m' # cyan
275
+
276
+ line = f" {type_color}{rtype:<8}\033[0m {score:<6} {concept_str:<15} {content}"
277
+ line = line[:width+15]
278
+
279
+ if idx == selected:
280
+ sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
281
+ else:
282
+ sys.stdout.write(f' {line}')
283
+
284
+ # Status bar
285
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
286
+ sel = display_results[selected] if display_results else {}
287
+ source = sel.get('source', '')
288
+ sys.stdout.write(f'\033[{height-1};1H\033[K Source: {source}'.ljust(width))
289
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav 1/2/3:Sort c:Concepts e:Explore p:Preview q:Quit [{selected+1}/{len(display_results)}] \033[0m')
290
+
291
+ else: # preview mode
292
+ sel = display_results[selected]
293
+ content = sel.get('content', '')
294
+ lines = content.split('\n')
295
+
296
+ # Add metadata at top
297
+ meta_lines = [
298
+ f"Type: {sel.get('type', '')}",
299
+ f"Score: {sel.get('score', '')}",
300
+ f"Concept: {sel.get('concept', '')}",
301
+ f"Source: {sel.get('source', '')}",
302
+ "─" * 40,
303
+ ]
304
+ all_lines = meta_lines + lines
305
+
306
+ for i in range(list_height):
307
+ idx = preview_scroll + i
308
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
309
+ if idx < len(all_lines):
310
+ sys.stdout.write(all_lines[idx][:width-1])
311
+
312
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
313
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(all_lines)} lines]')
314
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back e:Explore q:Quit \033[0m')
315
+
316
+ sys.stdout.flush()
317
+
318
+ c = sys.stdin.read(1)
319
+
320
+ if c == '\x1b':
321
+ c2 = sys.stdin.read(1)
322
+ if c2 == '[':
323
+ c3 = sys.stdin.read(1)
324
+ if c3 == 'A': # Up
325
+ if mode == 'list' and selected > 0:
326
+ selected -= 1
327
+ elif mode == 'preview' and preview_scroll > 0:
328
+ preview_scroll -= 1
329
+ elif c3 == 'B': # Down
330
+ if mode == 'list' and selected < len(display_results) - 1:
331
+ selected += 1
332
+ elif mode == 'preview':
333
+ sel = display_results[selected]
334
+ content = sel.get('content', '')
335
+ all_lines = content.split('\n')
336
+ if preview_scroll < max(0, len(all_lines) + 5 - list_height):
337
+ preview_scroll += 1
338
+ else:
339
+ if mode == 'preview':
340
+ mode = 'list'
341
+ sys.stdout.write('\033[2J\033[H')
342
+ else:
343
+ context['output'] = "Cancelled."
344
+ break
345
+ continue
346
+
347
+ if c == 'q' or c == '\x03':
348
+ context['output'] = "Cancelled."
349
+ break
350
+ elif c == 'k':
351
+ if mode == 'list' and selected > 0:
352
+ selected -= 1
353
+ elif mode == 'preview' and preview_scroll > 0:
354
+ preview_scroll -= 1
355
+ elif c == 'j':
356
+ if mode == 'list' and selected < len(display_results) - 1:
357
+ selected += 1
358
+ elif mode == 'preview':
359
+ sel = display_results[selected]
360
+ content = sel.get('content', '')
361
+ all_lines = content.split('\n')
362
+ if preview_scroll < max(0, len(all_lines) + 5 - list_height):
363
+ preview_scroll += 1
364
+ elif c == '1':
365
+ sort_mode = 'score'
366
+ display_results = filter_results(sort_results(results, sort_mode), type_filter)
367
+ selected = 0
368
+ scroll = 0
369
+ elif c == '2':
370
+ sort_mode = 'concept'
371
+ display_results = filter_results(sort_results(results, sort_mode), type_filter)
372
+ selected = 0
373
+ scroll = 0
374
+ elif c == '3':
375
+ sort_mode = 'type'
376
+ display_results = filter_results(sort_results(results, sort_mode), type_filter)
377
+ selected = 0
378
+ scroll = 0
379
+ elif c == 'c' and mode == 'list':
380
+ # Toggle type filter
381
+ if type_filter == 'all':
382
+ type_filter = 'concept'
383
+ elif type_filter == 'concept':
384
+ type_filter = 'fact'
385
+ else:
386
+ type_filter = 'all'
387
+ display_results = filter_results(sort_results(results, sort_mode), type_filter)
388
+ selected = 0
389
+ scroll = 0
390
+ elif c == 'e' and display_results:
391
+ # Explore the selected concept
392
+ sel = display_results[selected]
393
+ explore_concept = sel.get('concept') or sel.get('content', '')
394
+ if sel.get('type') == 'concept':
395
+ explore_concept = sel.get('content', '')
396
+ context['output'] = f"Explore concept: {explore_concept}\n\nRun: /kg_search concept={explore_concept}"
397
+ break
398
+ elif c == 'p' and mode == 'list' and display_results:
399
+ mode = 'preview'
400
+ preview_scroll = 0
401
+ sys.stdout.write('\033[2J\033[H')
402
+ elif c == 'b' and mode == 'preview':
403
+ mode = 'list'
404
+ sys.stdout.write('\033[2J\033[H')
405
+ elif c in ('\r', '\n') and display_results:
406
+ sel = display_results[selected]
407
+ context['output'] = f"Selected: {sel.get('content', '')[:100]}"
408
+ break
409
+
410
+ finally:
411
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
412
+ sys.stdout.write('\033[?25h')
413
+ sys.stdout.write('\033[2J\033[H')
414
+ sys.stdout.flush()
415
+
416
+ except Exception as e:
417
+ import traceback
418
+ context['output'] = "KG search error: " + str(e) + "\n" + traceback.format_exc()
@@ -1,4 +1,4 @@
1
- jinx_name: studio.list_panes
1
+ jinx_name: studio_list_panes
2
2
  description: List all open panes in NPC Studio. Returns pane IDs, types, titles, and which is active.
3
3
  inputs: []
4
4
  steps:
@@ -0,0 +1,73 @@
1
+ jinx_name: mem_review
2
+ description: Review pending memories with interactive UI
3
+ inputs:
4
+ - limit: "20"
5
+ - npc_filter: ""
6
+ - team_filter: ""
7
+ - db_path: ""
8
+
9
+ steps:
10
+ - name: review_memories
11
+ engine: python
12
+ code: |
13
+ import os
14
+ from npcpy.memory.command_history import CommandHistory
15
+ from npcpy.memory.memory_processor import memory_batch_review_ui
16
+
17
+ limit = int(context.get('limit') or 20)
18
+ npc_filter = context.get('npc_filter') or None
19
+ team_filter = context.get('team_filter') or None
20
+ db_path = context.get('db_path') or os.path.expanduser("~/npcsh_history.db")
21
+
22
+ # Get current npc/team from context if not specified
23
+ if not npc_filter:
24
+ try:
25
+ npc_filter = npc.name if 'npc' in dir() and npc else None
26
+ except:
27
+ pass
28
+
29
+ if not team_filter:
30
+ try:
31
+ team_filter = state.team.name if 'state' in dir() and state and state.team else None
32
+ except:
33
+ pass
34
+
35
+ try:
36
+ cmd_history = CommandHistory(db_path)
37
+
38
+ # Show what we're about to review
39
+ pending = cmd_history.get_pending_memories(limit=limit)
40
+ if npc_filter:
41
+ pending = [m for m in pending if m.get('npc') == npc_filter]
42
+ if team_filter:
43
+ pending = [m for m in pending if m.get('team') == team_filter]
44
+
45
+ if not pending:
46
+ context['output'] = "No pending memories to review."
47
+ else:
48
+ print(f"\nFound {len(pending)} pending memories")
49
+ if npc_filter:
50
+ print(f" NPC filter: {npc_filter}")
51
+ if team_filter:
52
+ print(f" Team filter: {team_filter}")
53
+
54
+ # Run interactive review
55
+ stats = memory_batch_review_ui(
56
+ cmd_history,
57
+ npc_filter=npc_filter,
58
+ team_filter=team_filter,
59
+ limit=limit
60
+ )
61
+
62
+ lines = [
63
+ "Review complete:",
64
+ f" Approved: {stats.get('approved', 0)}",
65
+ f" Rejected: {stats.get('rejected', 0)}",
66
+ f" Edited: {stats.get('edited', 0)}",
67
+ f" Skipped: {stats.get('skipped', 0)}",
68
+ ]
69
+ context['output'] = "\n".join(lines)
70
+
71
+ except Exception as e:
72
+ import traceback
73
+ context['output'] = "Memory review error: " + str(e) + "\n" + traceback.format_exc()