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,430 @@
1
+ jinx_name: corca
2
+ description: MCP-powered agentic shell - LLM with tool use via MCP servers
3
+ inputs:
4
+ - mcp_server_path: null
5
+ - initial_command: null
6
+ - model: null
7
+ - provider: null
8
+
9
+ steps:
10
+ - name: corca_repl
11
+ engine: python
12
+ code: |
13
+ import os
14
+ import sys
15
+ import tty
16
+ import termios
17
+ import asyncio
18
+ import json
19
+ from contextlib import AsyncExitStack
20
+ from termcolor import colored
21
+
22
+ from npcpy.llm_funcs import get_llm_response
23
+ from npcpy.npc_sysenv import render_markdown, get_system_message
24
+
25
+ # MCP imports
26
+ try:
27
+ from mcp import ClientSession, StdioServerParameters
28
+ from mcp.client.stdio import stdio_client
29
+ MCP_AVAILABLE = True
30
+ except ImportError:
31
+ MCP_AVAILABLE = False
32
+ print(colored("MCP not available. Install with: pip install mcp-client", "yellow"))
33
+
34
+ npc = context.get('npc')
35
+ team = context.get('team')
36
+ messages = context.get('messages', [])
37
+ mcp_server_path = context.get('mcp_server_path')
38
+ initial_command = context.get('initial_command')
39
+
40
+ # Resolve npc if it's a string (npc name) rather than NPC object
41
+ if isinstance(npc, str) and team:
42
+ npc = team.get(npc) if hasattr(team, 'get') else None
43
+ elif isinstance(npc, str):
44
+ npc = None
45
+
46
+ model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
47
+ provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
48
+
49
+ # Use shared_context for MCP state
50
+ shared_ctx = npc.shared_context if npc and hasattr(npc, 'shared_context') else {}
51
+
52
+ # ========== TUI Helper Functions ==========
53
+ def get_terminal_size():
54
+ try:
55
+ size = os.get_terminal_size()
56
+ return size.columns, size.lines
57
+ except:
58
+ return 80, 24
59
+
60
+ def tools_tui_browser(tools_llm):
61
+ """Interactive TUI browser for MCP tools"""
62
+ if not tools_llm:
63
+ print(colored("No MCP tools connected.", "yellow"))
64
+ return None
65
+
66
+ # Build tool info list
67
+ tools = []
68
+ for t in tools_llm:
69
+ func = t.get('function', {})
70
+ tools.append({
71
+ 'name': func.get('name', 'unknown'),
72
+ 'description': func.get('description', '')[:100],
73
+ 'params': func.get('parameters', {})
74
+ })
75
+
76
+ width, height = get_terminal_size()
77
+ selected = 0
78
+ scroll = 0
79
+ list_height = height - 5
80
+ mode = 'list'
81
+ preview_scroll = 0
82
+ preview_lines = []
83
+
84
+ fd = sys.stdin.fileno()
85
+ old_settings = termios.tcgetattr(fd)
86
+
87
+ try:
88
+ tty.setcbreak(fd)
89
+ sys.stdout.write('\033[?25l')
90
+ sys.stdout.write('\033[2J\033[H')
91
+
92
+ while True:
93
+ width, height = get_terminal_size()
94
+ list_height = height - 5
95
+
96
+ if mode == 'list':
97
+ if selected < scroll:
98
+ scroll = selected
99
+ elif selected >= scroll + list_height:
100
+ scroll = selected - list_height + 1
101
+
102
+ sys.stdout.write('\033[H')
103
+
104
+ # Header
105
+ if mode == 'list':
106
+ header = f" CORCA MCP TOOLS ({len(tools)} available) "
107
+ else:
108
+ header = f" TOOL: {tools[selected]['name']} "
109
+ sys.stdout.write(f'\033[46;30;1m{header.ljust(width)}\033[0m\n')
110
+
111
+ if mode == 'list':
112
+ col_header = f' {"NAME":<25} {"DESCRIPTION":<50}'
113
+ sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
114
+ else:
115
+ sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
116
+
117
+ if mode == 'list':
118
+ for i in range(list_height):
119
+ idx = scroll + i
120
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
121
+ if idx >= len(tools):
122
+ continue
123
+
124
+ t = tools[idx]
125
+ name = t['name'][:25]
126
+ desc = t['description'][:50]
127
+
128
+ line = f" {name:<25} {desc}"
129
+ line = line[:width-1]
130
+
131
+ if idx == selected:
132
+ sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
133
+ else:
134
+ sys.stdout.write(f' {line}')
135
+
136
+ # Status bar
137
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
138
+ t = tools[selected] if tools else {}
139
+ params = t.get('params', {}).get('properties', {})
140
+ param_names = list(params.keys())[:5]
141
+ sys.stdout.write(f'\033[{height-1};1H\033[K Params: {", ".join(param_names) if param_names else "none"}'.ljust(width)[:width])
142
+ sys.stdout.write(f'\033[{height};1H\033[K\033[46;30m j/k:Nav p:Details Enter:Copy q:Quit [{selected+1}/{len(tools)}] \033[0m')
143
+
144
+ else: # preview mode
145
+ for i in range(list_height):
146
+ idx = preview_scroll + i
147
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
148
+ if idx < len(preview_lines):
149
+ sys.stdout.write(preview_lines[idx][:width-1])
150
+
151
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
152
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(preview_lines)} lines]')
153
+ sys.stdout.write(f'\033[{height};1H\033[K\033[46;30m j/k:Scroll b:Back q:Quit \033[0m')
154
+
155
+ sys.stdout.flush()
156
+
157
+ c = sys.stdin.read(1)
158
+
159
+ if c == '\x1b':
160
+ c2 = sys.stdin.read(1)
161
+ if c2 == '[':
162
+ c3 = sys.stdin.read(1)
163
+ if c3 == 'A': # Up
164
+ if mode == 'list' and selected > 0:
165
+ selected -= 1
166
+ elif mode == 'preview' and preview_scroll > 0:
167
+ preview_scroll -= 1
168
+ elif c3 == 'B': # Down
169
+ if mode == 'list' and selected < len(tools) - 1:
170
+ selected += 1
171
+ elif mode == 'preview' and preview_scroll < max(0, len(preview_lines) - list_height):
172
+ preview_scroll += 1
173
+ else:
174
+ if mode == 'preview':
175
+ mode = 'list'
176
+ sys.stdout.write('\033[2J\033[H')
177
+ else:
178
+ return None
179
+ continue
180
+
181
+ if c == 'q' or c == '\x03':
182
+ return None
183
+ elif c == 'k':
184
+ if mode == 'list' and selected > 0:
185
+ selected -= 1
186
+ elif mode == 'preview' and preview_scroll > 0:
187
+ preview_scroll -= 1
188
+ elif c == 'j':
189
+ if mode == 'list' and selected < len(tools) - 1:
190
+ selected += 1
191
+ elif mode == 'preview' and preview_scroll < max(0, len(preview_lines) - list_height):
192
+ preview_scroll += 1
193
+ elif c == 'p' and mode == 'list' and tools:
194
+ # Preview tool details
195
+ t = tools[selected]
196
+ preview_str = f"Tool: {t['name']}\n"
197
+ preview_str += f"{'=' * 40}\n\n"
198
+ preview_str += f"Description:\n{t['description']}\n\n"
199
+ preview_str += f"Parameters:\n"
200
+ params = t.get('params', {})
201
+ props = params.get('properties', {})
202
+ required = params.get('required', [])
203
+ for pname, pinfo in props.items():
204
+ req = "*" if pname in required else ""
205
+ ptype = pinfo.get('type', 'any')
206
+ pdesc = pinfo.get('description', '')[:60]
207
+ preview_str += f" {pname}{req} ({ptype}): {pdesc}\n"
208
+ if not props:
209
+ preview_str += " (no parameters)\n"
210
+ preview_lines = preview_str.split('\n')
211
+ mode = 'preview'
212
+ preview_scroll = 0
213
+ sys.stdout.write('\033[2J\033[H')
214
+ elif c == 'b' and mode == 'preview':
215
+ mode = 'list'
216
+ sys.stdout.write('\033[2J\033[H')
217
+ elif c in ('\r', '\n') and mode == 'list' and tools:
218
+ # Return tool name for use
219
+ return tools[selected]['name']
220
+
221
+ finally:
222
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
223
+ sys.stdout.write('\033[?25h')
224
+ sys.stdout.write('\033[2J\033[H')
225
+ sys.stdout.flush()
226
+
227
+ print("""
228
+ ██████╗ ██████╗ ██████╗ ██████╗ █████╗
229
+ ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗
230
+ ██║ ██║ ██║██████╔╝██║ ███████║
231
+ ██║ ██║ ██║██╔══██╗██║ ██╔══██╗
232
+ ╚██████╗╚██████╔╝██║ ██║╚██████╗██║ ██║
233
+ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
234
+ """)
235
+
236
+ npc_name = npc.name if npc else "corca"
237
+ print(f"Entering corca mode (NPC: {npc_name}). Type '/cq' to exit.")
238
+
239
+ # ========== MCP Connection Setup ==========
240
+ async def connect_mcp(server_path):
241
+ """Connect to MCP server and return tools"""
242
+ if not MCP_AVAILABLE:
243
+ return [], {}
244
+
245
+ abs_path = os.path.abspath(os.path.expanduser(server_path))
246
+ if not os.path.exists(abs_path):
247
+ print(colored(f"MCP server not found: {abs_path}", "red"))
248
+ return [], {}
249
+
250
+ try:
251
+ loop = asyncio.get_event_loop()
252
+ except RuntimeError:
253
+ loop = asyncio.new_event_loop()
254
+ asyncio.set_event_loop(loop)
255
+
256
+ exit_stack = AsyncExitStack()
257
+
258
+ if abs_path.endswith('.py'):
259
+ cmd_parts = [sys.executable, abs_path]
260
+ else:
261
+ cmd_parts = [abs_path]
262
+
263
+ server_params = StdioServerParameters(
264
+ command=cmd_parts[0],
265
+ args=[abs_path],
266
+ env=os.environ.copy()
267
+ )
268
+
269
+ stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params))
270
+ session = await exit_stack.enter_async_context(ClientSession(*stdio_transport))
271
+ await session.initialize()
272
+
273
+ response = await session.list_tools()
274
+ tools_llm = []
275
+ tool_map = {}
276
+
277
+ if response.tools:
278
+ for mcp_tool in response.tools:
279
+ tool_def = {
280
+ "type": "function",
281
+ "function": {
282
+ "name": mcp_tool.name,
283
+ "description": mcp_tool.description or f"MCP tool: {mcp_tool.name}",
284
+ "parameters": getattr(mcp_tool, "inputSchema", {"type": "object", "properties": {}})
285
+ }
286
+ }
287
+ tools_llm.append(tool_def)
288
+
289
+ # Create sync wrapper for async tool call
290
+ def make_tool_func(tool_name, sess, lp):
291
+ async def call_tool(**kwargs):
292
+ cleaned = {k: (None if v == 'None' else v) for k, v in kwargs.items()}
293
+ result = await asyncio.wait_for(sess.call_tool(tool_name, cleaned), timeout=30.0)
294
+ return result
295
+ def sync_call(**kwargs):
296
+ return lp.run_until_complete(call_tool(**kwargs))
297
+ return sync_call
298
+
299
+ tool_map[mcp_tool.name] = make_tool_func(mcp_tool.name, session, loop)
300
+
301
+ # Store in shared context
302
+ shared_ctx['mcp_client'] = session
303
+ shared_ctx['mcp_tools'] = tools_llm
304
+ shared_ctx['mcp_tool_map'] = tool_map
305
+ shared_ctx['_mcp_exit_stack'] = exit_stack
306
+ shared_ctx['_mcp_loop'] = loop
307
+
308
+ print(colored(f"Connected to MCP server. Tools: {', '.join(tool_map.keys())}", "green"))
309
+ return tools_llm, tool_map
310
+
311
+ # Try to connect if server path provided
312
+ tools_llm = shared_ctx.get('mcp_tools', [])
313
+ tool_map = shared_ctx.get('mcp_tool_map', {})
314
+
315
+ if mcp_server_path and not tools_llm:
316
+ try:
317
+ loop = asyncio.get_event_loop()
318
+ except RuntimeError:
319
+ loop = asyncio.new_event_loop()
320
+ asyncio.set_event_loop(loop)
321
+ tools_llm, tool_map = loop.run_until_complete(connect_mcp(mcp_server_path))
322
+
323
+ # Find default MCP server if none provided
324
+ if not tools_llm:
325
+ default_paths = [
326
+ os.path.expanduser("~/.npcsh/npc_team/mcp_server.py"),
327
+ os.path.join(team.team_path, "mcp_server.py") if team and hasattr(team, 'team_path') else None,
328
+ ]
329
+ for path in default_paths:
330
+ if path and os.path.exists(path):
331
+ try:
332
+ loop = asyncio.get_event_loop()
333
+ except RuntimeError:
334
+ loop = asyncio.new_event_loop()
335
+ asyncio.set_event_loop(loop)
336
+ tools_llm, tool_map = loop.run_until_complete(connect_mcp(path))
337
+ if tools_llm:
338
+ break
339
+
340
+ # Ensure system message
341
+ if not messages or messages[0].get("role") != "system":
342
+ sys_msg = get_system_message(npc) if npc else "You are an AI assistant with access to tools."
343
+ if tools_llm:
344
+ sys_msg += f"\n\nYou have access to these tools: {', '.join(t['function']['name'] for t in tools_llm)}"
345
+ messages.insert(0, {"role": "system", "content": sys_msg})
346
+
347
+ # Handle initial command if provided (one-shot mode)
348
+ if initial_command:
349
+ resp = get_llm_response(
350
+ initial_command,
351
+ model=model,
352
+ provider=provider,
353
+ messages=messages,
354
+ tools=tools_llm if tools_llm else None,
355
+ tool_map=tool_map if tool_map else None,
356
+ auto_process_tool_calls=True,
357
+ npc=npc
358
+ )
359
+ messages = resp.get('messages', messages)
360
+ render_markdown(str(resp.get('response', '')))
361
+ context['output'] = resp.get('response', 'Done.')
362
+ context['messages'] = messages
363
+ # Don't enter REPL for one-shot
364
+ exit()
365
+
366
+ # REPL loop
367
+ while True:
368
+ try:
369
+ prompt_str = f"{npc_name}:corca> "
370
+ user_input = input(prompt_str).strip()
371
+
372
+ if not user_input:
373
+ continue
374
+
375
+ if user_input.lower() == "/cq":
376
+ print("Exiting corca mode.")
377
+ break
378
+
379
+ # Handle /tools to browse available tools with TUI
380
+ if user_input.lower() == "/tools":
381
+ result = tools_tui_browser(tools_llm)
382
+ if result:
383
+ print(colored(f"Selected tool: {result}", "cyan"))
384
+ print(colored("Use it by describing what you want to do.", "gray"))
385
+ continue
386
+
387
+ # Handle /connect to connect to new MCP server
388
+ if user_input.startswith("/connect "):
389
+ new_path = user_input[9:].strip()
390
+ try:
391
+ loop = asyncio.get_event_loop()
392
+ except RuntimeError:
393
+ loop = asyncio.new_event_loop()
394
+ asyncio.set_event_loop(loop)
395
+ tools_llm, tool_map = loop.run_until_complete(connect_mcp(new_path))
396
+ continue
397
+
398
+ # Get LLM response with tools
399
+ resp = get_llm_response(
400
+ user_input,
401
+ model=model,
402
+ provider=provider,
403
+ messages=messages,
404
+ tools=tools_llm if tools_llm else None,
405
+ tool_map=tool_map if tool_map else None,
406
+ auto_process_tool_calls=True,
407
+ stream=False, # Tool calls don't work well with streaming
408
+ npc=npc
409
+ )
410
+
411
+ messages = resp.get('messages', messages)
412
+ response_text = resp.get('response', '')
413
+ render_markdown(str(response_text))
414
+
415
+ # Track usage
416
+ if 'usage' in resp and npc and hasattr(npc, 'shared_context'):
417
+ usage = resp['usage']
418
+ npc.shared_context['session_input_tokens'] += usage.get('input_tokens', 0)
419
+ npc.shared_context['session_output_tokens'] += usage.get('output_tokens', 0)
420
+ npc.shared_context['turn_count'] += 1
421
+
422
+ except KeyboardInterrupt:
423
+ print("\nUse '/cq' to exit or continue.")
424
+ continue
425
+ except EOFError:
426
+ print("\nExiting corca mode.")
427
+ break
428
+
429
+ context['output'] = "Exited corca mode."
430
+ context['messages'] = messages