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.
- npcsh/_state.py +122 -91
- npcsh/alicanto.py +2 -2
- npcsh/benchmark/__init__.py +8 -2
- npcsh/benchmark/npcsh_agent.py +87 -22
- npcsh/benchmark/runner.py +85 -43
- npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
- npcsh/build.py +2 -4
- npcsh/completion.py +2 -6
- npcsh/config.py +2 -3
- npcsh/conversation_viewer.py +389 -0
- npcsh/corca.py +0 -1
- npcsh/diff_viewer.py +452 -0
- npcsh/execution.py +0 -1
- npcsh/guac.py +0 -1
- npcsh/mcp_helpers.py +2 -3
- npcsh/mcp_server.py +5 -10
- npcsh/npc.py +10 -11
- npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
- npcsh/npc_team/jinxs/bin/config_tui.jinx +299 -0
- npcsh/npc_team/jinxs/bin/memories.jinx +316 -0
- npcsh/npc_team/jinxs/bin/setup.jinx +240 -0
- npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
- npcsh/npc_team/jinxs/bin/team_tui.jinx +327 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
- npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
- npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
- npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
- npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
- npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
- npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
- npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
- npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
- npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
- npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
- npcsh/npc_team/jinxs/modes/guac.jinx +542 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
- npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
- npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
- npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
- npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
- npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
- npcsh/npcsh.py +7 -4
- npcsh/plonk.py +0 -1
- npcsh/pti.py +0 -1
- npcsh/routes.py +1 -3
- npcsh/spool.py +0 -1
- npcsh/ui.py +0 -1
- npcsh/wander.py +0 -1
- npcsh/yap.py +0 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +356 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/arxiv.jinx +720 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +299 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/corca.jinx +430 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/db_search.jinx +348 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/file_search.jinx +339 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/guac.jinx +542 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +331 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/kg_search.jinx +418 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +73 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +388 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +412 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +379 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/pti.jinx +357 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/reattach.jinx +291 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/send_message.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/setup.jinx +240 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +22 -11
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/spool.jinx +350 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/sql.jinx +20 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/sync.jinx +223 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +327 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +455 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/web_search.jinx +283 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +13 -7
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/METADATA +110 -14
- npcsh-1.1.19.dist-info/RECORD +244 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/WHEEL +1 -1
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -3
- npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
- npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
- npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
- npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
- npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
- npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
- npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
- npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
- npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
- npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
- npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
- npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
- npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
- npcsh-1.1.17.data/data/npcsh/npc_team/sync.jinx +0 -230
- npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
- npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
- npcsh-1.1.17.dist-info/RECORD +0 -219
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.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
|