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,389 @@
1
+ """
2
+ Interactive conversation viewer for /reattach command.
3
+ Provides a TUI for browsing and selecting previous conversations.
4
+ """
5
+ import os
6
+ import sys
7
+ import tty
8
+ import termios
9
+ from datetime import datetime
10
+ from typing import List, Dict, Optional, Tuple
11
+
12
+ from termcolor import colored
13
+
14
+
15
+ def get_terminal_size() -> Tuple[int, int]:
16
+ """Get terminal width and height."""
17
+ try:
18
+ size = os.get_terminal_size()
19
+ return size.columns, size.lines
20
+ except:
21
+ return 80, 24
22
+
23
+
24
+ def clear_screen():
25
+ """Clear the terminal screen."""
26
+ sys.stdout.write('\033[2J\033[H')
27
+ sys.stdout.flush()
28
+
29
+
30
+ def move_cursor(row: int, col: int):
31
+ """Move cursor to specific position."""
32
+ sys.stdout.write(f'\033[{row};{col}H')
33
+ sys.stdout.flush()
34
+
35
+
36
+ def hide_cursor():
37
+ """Hide the cursor."""
38
+ sys.stdout.write('\033[?25l')
39
+ sys.stdout.flush()
40
+
41
+
42
+ def show_cursor():
43
+ """Show the cursor."""
44
+ sys.stdout.write('\033[?25h')
45
+ sys.stdout.flush()
46
+
47
+
48
+ def getch() -> str:
49
+ """Read a single character from stdin."""
50
+ fd = sys.stdin.fileno()
51
+ old_settings = termios.tcgetattr(fd)
52
+ try:
53
+ tty.setraw(fd)
54
+ ch = sys.stdin.read(1)
55
+ # Handle escape sequences
56
+ if ch == '\x1b':
57
+ ch2 = sys.stdin.read(1)
58
+ if ch2 == '[':
59
+ ch3 = sys.stdin.read(1)
60
+ return f'\x1b[{ch3}'
61
+ return ch
62
+ finally:
63
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
64
+
65
+
66
+ def truncate(text: str, max_len: int) -> str:
67
+ """Truncate text with ellipsis if needed."""
68
+ if len(text) <= max_len:
69
+ return text
70
+ return text[:max_len-3] + '...'
71
+
72
+
73
+ def format_timestamp(ts: str) -> str:
74
+ """Format timestamp for display."""
75
+ if not ts:
76
+ return 'unknown'
77
+ try:
78
+ # Try parsing ISO format
79
+ if 'T' in ts:
80
+ dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
81
+ else:
82
+ dt = datetime.strptime(ts[:19], '%Y-%m-%d %H:%M:%S')
83
+
84
+ now = datetime.now()
85
+ diff = now - dt.replace(tzinfo=None)
86
+
87
+ if diff.days == 0:
88
+ return f"Today {dt.strftime('%H:%M')}"
89
+ elif diff.days == 1:
90
+ return f"Yesterday {dt.strftime('%H:%M')}"
91
+ elif diff.days < 7:
92
+ return dt.strftime('%a %H:%M')
93
+ else:
94
+ return dt.strftime('%b %d')
95
+ except:
96
+ return ts[:16] if len(ts) > 16 else ts
97
+
98
+
99
+ class ConversationViewer:
100
+ """Interactive TUI for browsing conversations."""
101
+
102
+ def __init__(self, conversations: List[Dict], current_path: str):
103
+ self.conversations = conversations
104
+ self.current_path = current_path
105
+ self.selected = 0
106
+ self.scroll_offset = 0
107
+ self.preview_conversation = None
108
+ self.mode = 'list' # 'list' or 'preview'
109
+ self.preview_scroll = 0
110
+ self.width, self.height = get_terminal_size()
111
+
112
+ def draw_header(self):
113
+ """Draw the header bar."""
114
+ move_cursor(1, 1)
115
+ header = f" CONVERSATIONS: {truncate(self.current_path, self.width - 20)} "
116
+ header = header.ljust(self.width)
117
+ sys.stdout.write(colored(header, 'white', 'on_blue', attrs=['bold']))
118
+
119
+ def draw_help(self):
120
+ """Draw the help bar at bottom."""
121
+ move_cursor(self.height, 1)
122
+ if self.mode == 'list':
123
+ help_text = " ↑/↓:Navigate Enter:Select p:Preview q:Quit "
124
+ else:
125
+ help_text = " ↑/↓:Scroll b:Back Enter:Select q:Quit "
126
+ help_text = help_text.ljust(self.width)
127
+ sys.stdout.write(colored(help_text, 'white', 'on_blue'))
128
+
129
+ def draw_conversation_list(self):
130
+ """Draw the conversation list."""
131
+ list_height = self.height - 4 # Header, separator, status, help
132
+
133
+ # Calculate visible range
134
+ if self.selected < self.scroll_offset:
135
+ self.scroll_offset = self.selected
136
+ elif self.selected >= self.scroll_offset + list_height:
137
+ self.scroll_offset = self.selected - list_height + 1
138
+
139
+ for i in range(list_height):
140
+ row = 3 + i
141
+ move_cursor(row, 1)
142
+
143
+ idx = self.scroll_offset + i
144
+ if idx >= len(self.conversations):
145
+ sys.stdout.write(' ' * self.width)
146
+ continue
147
+
148
+ conv = self.conversations[idx]
149
+ is_selected = idx == self.selected
150
+
151
+ # Format conversation line
152
+ convo_id = conv.get('conversation_id', '')[:12]
153
+ msg_count = conv.get('msg_count', 0)
154
+ last_msg = format_timestamp(conv.get('last_msg', ''))
155
+ npcs = conv.get('npcs', 'default')
156
+ if npcs and len(npcs) > 15:
157
+ npcs = npcs[:12] + '...'
158
+
159
+ # Build line
160
+ prefix = '>' if is_selected else ' '
161
+ line = f"{prefix} {convo_id:<14} {msg_count:>4} msgs {last_msg:<15} {npcs}"
162
+ line = truncate(line, self.width - 1)
163
+ line = line.ljust(self.width - 1)
164
+
165
+ if is_selected:
166
+ sys.stdout.write(colored(line, 'black', 'on_white', attrs=['bold']))
167
+ else:
168
+ sys.stdout.write(line)
169
+
170
+ # Draw separator
171
+ move_cursor(self.height - 2, 1)
172
+ sys.stdout.write(colored('─' * self.width, 'grey'))
173
+
174
+ # Draw status
175
+ move_cursor(self.height - 1, 1)
176
+ if self.conversations:
177
+ conv = self.conversations[self.selected]
178
+ full_id = conv.get('conversation_id', '')
179
+ status = f" ID: {full_id}"
180
+ else:
181
+ status = " No conversations found"
182
+ sys.stdout.write(truncate(status, self.width).ljust(self.width))
183
+
184
+ def draw_preview(self):
185
+ """Draw conversation preview."""
186
+ if not self.preview_conversation:
187
+ return
188
+
189
+ preview_height = self.height - 4
190
+ messages = self.preview_conversation
191
+
192
+ # Draw messages
193
+ line_num = 0
194
+ for msg in messages:
195
+ if line_num >= self.preview_scroll + preview_height:
196
+ break
197
+ if line_num < self.preview_scroll:
198
+ line_num += 1
199
+ continue
200
+
201
+ row = 3 + (line_num - self.preview_scroll)
202
+ move_cursor(row, 1)
203
+
204
+ role = msg.get('role', 'unknown')
205
+ content = msg.get('content', '')[:200].replace('\n', ' ')
206
+
207
+ if role == 'user':
208
+ prefix = colored('You: ', 'green', attrs=['bold'])
209
+ elif role == 'assistant':
210
+ prefix = colored('AI: ', 'blue', attrs=['bold'])
211
+ else:
212
+ prefix = colored(f'{role}: ', 'grey')
213
+
214
+ line = truncate(content, self.width - 8)
215
+ sys.stdout.write(prefix + line.ljust(self.width - 6))
216
+ line_num += 1
217
+
218
+ # Clear remaining lines
219
+ for i in range(line_num - self.preview_scroll, preview_height):
220
+ move_cursor(3 + i, 1)
221
+ sys.stdout.write(' ' * self.width)
222
+
223
+ # Draw separator and status
224
+ move_cursor(self.height - 2, 1)
225
+ sys.stdout.write(colored('─' * self.width, 'grey'))
226
+ move_cursor(self.height - 1, 1)
227
+ status = f" Preview: {len(messages)} messages (scroll: {self.preview_scroll})"
228
+ sys.stdout.write(truncate(status, self.width).ljust(self.width))
229
+
230
+ def draw(self):
231
+ """Draw the full interface."""
232
+ self.draw_header()
233
+ if self.mode == 'list':
234
+ self.draw_conversation_list()
235
+ else:
236
+ self.draw_preview()
237
+ self.draw_help()
238
+ sys.stdout.flush()
239
+
240
+ def load_preview(self, fetch_messages_func):
241
+ """Load messages for preview."""
242
+ if not self.conversations:
243
+ return
244
+ conv = self.conversations[self.selected]
245
+ convo_id = conv.get('conversation_id')
246
+ if convo_id and fetch_messages_func:
247
+ self.preview_conversation = fetch_messages_func(convo_id)
248
+ self.preview_scroll = 0
249
+
250
+ def run(self, fetch_messages_func=None) -> Optional[str]:
251
+ """
252
+ Run the interactive viewer.
253
+
254
+ Args:
255
+ fetch_messages_func: Function to fetch messages for a conversation_id
256
+
257
+ Returns:
258
+ Selected conversation_id or None if cancelled
259
+ """
260
+ if not self.conversations:
261
+ print(colored("No conversations found for this path.", 'yellow'))
262
+ return None
263
+
264
+ old_settings = None
265
+ try:
266
+ # Setup terminal
267
+ old_settings = termios.tcgetattr(sys.stdin.fileno())
268
+ hide_cursor()
269
+ clear_screen()
270
+
271
+ while True:
272
+ self.draw()
273
+
274
+ key = getch()
275
+
276
+ if key == 'q' or key == '\x03': # q or Ctrl+C
277
+ return None
278
+
279
+ elif key == '\x1b[A': # Up
280
+ if self.mode == 'list':
281
+ if self.selected > 0:
282
+ self.selected -= 1
283
+ else:
284
+ if self.preview_scroll > 0:
285
+ self.preview_scroll -= 1
286
+
287
+ elif key == '\x1b[B': # Down
288
+ if self.mode == 'list':
289
+ if self.selected < len(self.conversations) - 1:
290
+ self.selected += 1
291
+ else:
292
+ self.preview_scroll += 1
293
+
294
+ elif key == '\r' or key == '\n': # Enter
295
+ if self.conversations:
296
+ return self.conversations[self.selected].get('conversation_id')
297
+
298
+ elif key == 'p' and self.mode == 'list':
299
+ self.load_preview(fetch_messages_func)
300
+ if self.preview_conversation:
301
+ self.mode = 'preview'
302
+ clear_screen()
303
+
304
+ elif key == 'b' and self.mode == 'preview':
305
+ self.mode = 'list'
306
+ clear_screen()
307
+
308
+ elif key == 'j': # vim-style down
309
+ if self.mode == 'list' and self.selected < len(self.conversations) - 1:
310
+ self.selected += 1
311
+ elif self.mode == 'preview':
312
+ self.preview_scroll += 1
313
+
314
+ elif key == 'k': # vim-style up
315
+ if self.mode == 'list' and self.selected > 0:
316
+ self.selected -= 1
317
+ elif self.mode == 'preview' and self.preview_scroll > 0:
318
+ self.preview_scroll -= 1
319
+
320
+ except Exception:
321
+ return None
322
+ finally:
323
+ # Restore terminal
324
+ show_cursor()
325
+ clear_screen()
326
+ if old_settings:
327
+ termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_settings)
328
+
329
+
330
+ def launch_conversation_viewer(
331
+ db_path: str,
332
+ target_path: str,
333
+ limit: int = 50
334
+ ) -> Optional[str]:
335
+ """
336
+ Launch the conversation viewer and return selected conversation_id.
337
+
338
+ Args:
339
+ db_path: Path to the npcsh database
340
+ target_path: Directory path to filter conversations
341
+ limit: Maximum number of conversations to show
342
+
343
+ Returns:
344
+ Selected conversation_id or None
345
+ """
346
+ from sqlalchemy import create_engine, text
347
+
348
+ engine = create_engine(f'sqlite:///{db_path}')
349
+
350
+ # Fetch conversations
351
+ with engine.connect() as conn:
352
+ result = conn.execute(text("""
353
+ SELECT conversation_id, directory_path,
354
+ MIN(timestamp) as started,
355
+ MAX(timestamp) as last_msg,
356
+ COUNT(*) as msg_count,
357
+ GROUP_CONCAT(DISTINCT npc) as npcs
358
+ FROM conversation_history
359
+ WHERE directory_path = :path OR directory_path LIKE :path_pattern
360
+ GROUP BY conversation_id
361
+ ORDER BY last_msg DESC
362
+ LIMIT :limit
363
+ """), {"path": target_path, "path_pattern": target_path + "/%", "limit": limit})
364
+
365
+ conversations = []
366
+ for row in result.fetchall():
367
+ conversations.append({
368
+ 'conversation_id': row[0],
369
+ 'directory_path': row[1],
370
+ 'started': row[2],
371
+ 'last_msg': row[3],
372
+ 'msg_count': row[4],
373
+ 'npcs': row[5]
374
+ })
375
+
376
+ def fetch_messages(convo_id: str) -> List[Dict]:
377
+ """Fetch messages for a conversation."""
378
+ with engine.connect() as conn:
379
+ result = conn.execute(text("""
380
+ SELECT role, content, timestamp, npc
381
+ FROM conversation_history
382
+ WHERE conversation_id = :convo_id
383
+ ORDER BY timestamp ASC
384
+ LIMIT 100
385
+ """), {"convo_id": convo_id})
386
+ return [dict(row._mapping) for row in result.fetchall()]
387
+
388
+ viewer = ConversationViewer(conversations, target_path)
389
+ return viewer.run(fetch_messages_func=fetch_messages)
npcsh/corca.py CHANGED
@@ -4,7 +4,6 @@ corca - MCP-powered agentic shell CLI entry point
4
4
  This is a thin wrapper that executes the corca.jinx through the jinx mechanism.
5
5
  """
6
6
  import argparse
7
- import os
8
7
  import sys
9
8
 
10
9
  from npcsh._state import setup_shell