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.
- npcsh/_state.py +114 -91
- npcsh/alicanto.py +2 -2
- npcsh/benchmark/__init__.py +8 -2
- npcsh/benchmark/npcsh_agent.py +46 -12
- 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 +1 -3
- npcsh/conversation_viewer.py +389 -0
- npcsh/corca.py +0 -1
- 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/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 +544 -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.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
- npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +22 -11
- npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.jinx +13 -7
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/METADATA +90 -1
- npcsh-1.1.18.dist-info/RECORD +235 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +0 -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/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.18.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/confirm.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/navigate.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/notify.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/search.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/send_message.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/write_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
- {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
npcsh/execution.py
CHANGED
npcsh/guac.py
CHANGED
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|