npcsh 1.1.17__py3-none-any.whl → 1.1.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. npcsh/_state.py +114 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +46 -12
  5. npcsh/benchmark/runner.py +85 -43
  6. npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
  7. npcsh/build.py +2 -4
  8. npcsh/completion.py +2 -6
  9. npcsh/config.py +1 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/execution.py +0 -1
  13. npcsh/guac.py +0 -1
  14. npcsh/mcp_helpers.py +2 -3
  15. npcsh/mcp_server.py +5 -10
  16. npcsh/npc.py +10 -11
  17. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  18. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  19. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  20. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  21. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  22. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  23. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  24. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  25. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  26. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  27. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  28. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  29. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  30. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  31. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  32. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  33. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  34. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  35. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  36. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  37. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  38. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  39. npcsh/npcsh.py +7 -4
  40. npcsh/plonk.py +0 -1
  41. npcsh/pti.py +0 -1
  42. npcsh/routes.py +1 -3
  43. npcsh/spool.py +0 -1
  44. npcsh/ui.py +0 -1
  45. npcsh/wander.py +0 -1
  46. npcsh/yap.py +0 -1
  47. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  48. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  49. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  50. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  51. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  52. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  53. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  54. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  55. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  56. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  57. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  58. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  59. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  60. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  61. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  62. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  63. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  64. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  65. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  66. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  67. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  68. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.jinx +13 -7
  69. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/METADATA +90 -1
  70. npcsh-1.1.18.dist-info/RECORD +235 -0
  71. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  72. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +0 -3
  73. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  74. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  75. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  76. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  77. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  78. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  79. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  80. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  81. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  82. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  83. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  84. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  85. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  86. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  87. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  88. npcsh-1.1.17.dist-info/RECORD +0 -219
  89. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  90. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  91. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  92. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  93. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  94. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +0 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +0 -0
  96. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +0 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  98. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  100. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +0 -0
  102. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +0 -0
  103. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  104. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +0 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  106. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  107. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  108. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  109. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  110. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  112. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  113. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  114. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  115. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +0 -0
  116. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  117. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +0 -0
  118. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  119. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  120. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  121. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  122. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  123. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  124. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  125. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/notify.jinx +0 -0
  126. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  127. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  128. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +0 -0
  129. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  130. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  131. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +0 -0
  132. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  133. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  134. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  135. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/search.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  168. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  169. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -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
npcsh/execution.py CHANGED
@@ -4,7 +4,6 @@ Command execution utilities for npcsh
4
4
  import os
5
5
  import shutil
6
6
  import subprocess
7
- import sys
8
7
  from typing import List, Tuple, Any, Optional
9
8
 
10
9
  from termcolor import colored
npcsh/guac.py CHANGED
@@ -4,7 +4,6 @@ guac - Python data analysis mode CLI entry point
4
4
  This is a thin wrapper that executes the guac.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
npcsh/mcp_helpers.py CHANGED
@@ -4,7 +4,6 @@ Raw MCP client with no exception handling and full visibility.
4
4
  """
5
5
 
6
6
  import asyncio
7
- import os
8
7
  import sys
9
8
  import json
10
9
  try:
@@ -245,7 +244,7 @@ class MCPClient:
245
244
 
246
245
 
247
246
  self._log(f"Executing tool: {tool_name} with args: {tool_args}")
248
- print(f"\nExecuting tool call:")
247
+ print("\nExecuting tool call:")
249
248
  print(f" Jinx name: {tool_name}")
250
249
  print(f" Jinx args: {tool_args}")
251
250
  print(f" Jinx args type: {type(tool_args)}")
@@ -268,7 +267,7 @@ class MCPClient:
268
267
  print(f" TextContent detected, text: {tool_result.text}")
269
268
  tool_result = tool_result.text
270
269
  elif isinstance(tool_result, list) and all(hasattr(item, 'text') for item in tool_result):
271
- print(f" List of TextContent detected")
270
+ print(" List of TextContent detected")
272
271
  tool_result = [item.text for item in tool_result]
273
272
 
274
273
 
npcsh/mcp_server.py CHANGED
@@ -7,9 +7,8 @@ npcpy.llm_funcs, and npcpy.npc_compiler as tools.
7
7
  import os
8
8
  import subprocess
9
9
  import json
10
- import asyncio
11
10
 
12
- from typing import Optional, Dict, Any, List, Union, Callable
11
+ from typing import List, Callable
13
12
 
14
13
  from mcp.server.fastmcp import FastMCP
15
14
  import importlib
@@ -20,19 +19,15 @@ from sqlalchemy import text
20
19
  import os
21
20
  import subprocess
22
21
  import json
23
- import asyncio
24
22
  try:
25
23
  import inspect
26
24
  except:
27
25
  pass
28
- from typing import Optional, Dict, Any, List, Union, Callable, get_type_hints
26
+ from typing import List, Callable
29
27
 
30
28
  from functools import wraps
31
- import sys
32
29
 
33
- from npcpy.llm_funcs import generate_group_candidates, abstract,
34
- zoom_in, execute_llm_command, gen_image
35
- from npcpy.memory.search import search_similar_texts, execute_search_command, execute_rag_command, answer_with_rag, execute_brainblast_command
30
+ from npcpy.llm_funcs import (gen_image)
36
31
  from npcpy.data.load import load_file_contents
37
32
  from npcpy.memory.command_history import CommandHistory
38
33
  from npcpy.data.image import capture_screenshot
@@ -268,7 +263,7 @@ def register_selected_npcpy_tools():
268
263
  gen_image,
269
264
  load_file_contents,
270
265
  capture_screenshot,
271
- search_web, ]
266
+ search_web ]
272
267
 
273
268
  for func in tools:
274
269
 
@@ -293,7 +288,7 @@ register_selected_npcpy_tools()
293
288
 
294
289
 
295
290
  if __name__ == "__main__":
296
- print(f"Starting enhanced NPCPY MCP server...")
291
+ print("Starting enhanced NPCPY MCP server...")
297
292
  print(f"Workspace: {DEFAULT_WORKSPACE}")
298
293
 
299
294
 
npcsh/npc.py CHANGED
@@ -7,7 +7,6 @@ from typing import Optional
7
7
  from npcsh._state import (
8
8
  NPCSH_CHAT_MODEL,
9
9
  NPCSH_CHAT_PROVIDER,
10
- NPCSH_API_URL,
11
10
  NPCSH_DB_PATH,
12
11
  NPCSH_STREAM_OUTPUT,
13
12
  initial_state,
@@ -16,9 +15,8 @@ from npcpy.npc_sysenv import (
16
15
  print_and_process_stream_with_markdown,
17
16
  render_markdown,
18
17
  )
19
- from npcpy.npc_compiler import NPC, Team
18
+ from npcpy.npc_compiler import NPC
20
19
  from npcsh.routes import router
21
- from npcpy.llm_funcs import check_llm_command
22
20
  from sqlalchemy import create_engine
23
21
 
24
22
  from npcsh._state import (
@@ -256,8 +254,9 @@ def main():
256
254
  print(
257
255
  f"Processing prompt: '{prompt}' with NPC: '{args.npc}'..."
258
256
  )
259
-
260
- shell_state.current_mode = 'chat'
257
+
258
+ # Use NPCSH_DEFAULT_MODE environment variable, default to 'agent' for tool execution
259
+ shell_state.current_mode = os.environ.get('NPCSH_DEFAULT_MODE', 'agent')
261
260
  updated_state, result = execute_command(
262
261
  prompt,
263
262
  shell_state,
@@ -274,12 +273,12 @@ def main():
274
273
  )
275
274
 
276
275
  if (
277
- hasattr(output, '__iter__')
276
+ hasattr(output, '__iter__')
278
277
  and not isinstance(output, (str, bytes, dict, list))
279
278
  ):
280
- final_output = print_and_process_stream_with_markdown(
281
- output,
282
- model_for_stream,
279
+ print_and_process_stream_with_markdown(
280
+ output,
281
+ model_for_stream,
283
282
  provider_for_stream,
284
283
  show=True
285
284
  )
@@ -289,7 +288,7 @@ def main():
289
288
  hasattr(result, '__iter__')
290
289
  and not isinstance(result, (str, bytes, dict, list))
291
290
  ):
292
- final_output = print_and_process_stream_with_markdown(
291
+ print_and_process_stream_with_markdown(
293
292
  result,
294
293
  effective_model,
295
294
  effective_provider,
@@ -324,7 +323,7 @@ def jinx_main():
324
323
  if arg in ['-h', '--help']:
325
324
  print(f"Usage: {jinx_name} [key=value ...]")
326
325
  print(f"\nRun the '{jinx_name}' jinx with specified parameters.")
327
- print(f"\nExamples:")
326
+ print("\nExamples:")
328
327
  print(f" {jinx_name} show=1")
329
328
  print(f" {jinx_name} model=my_model db=~/mydb.db")
330
329
  print(f"\nOr use: npc {jinx_name} [key=value ...]")
@@ -21,7 +21,7 @@ steps:
21
21
  npc_name_input = {{ npc_name | default("") | tojson }}.strip() or None
22
22
 
23
23
  if not model:
24
- model = npc.model if npc and npc.model
24
+ model = npc.model if npc and npc.model else ""
25
25
  if not provider:
26
26
  provider = npc.provider if npc and npc.provider else "anthropic"
27
27