npcsh 1.1.18__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 +8 -0
- npcsh/benchmark/npcsh_agent.py +47 -16
- npcsh/config.py +1 -0
- npcsh/diff_viewer.py +452 -0
- 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/modes/guac.jinx +0 -2
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
- {npcsh-1.1.18.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.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.jinx +0 -2
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
- {npcsh-1.1.18.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.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
- {npcsh-1.1.18.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.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/METADATA +21 -14
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/RECORD +138 -129
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/sync.jinx +0 -230
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/db_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/file_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kg_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/mem_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paper_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/pti.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/reattach.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/semantic_scholar.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wander.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/web_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/WHEEL +0 -0
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
jinx_name: setup
|
|
2
|
+
description: Interactive setup wizard for npcsh - detect local models, configure defaults
|
|
3
|
+
inputs:
|
|
4
|
+
- skip_detection: ""
|
|
5
|
+
steps:
|
|
6
|
+
- name: setup_wizard
|
|
7
|
+
engine: python
|
|
8
|
+
code: |
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tty
|
|
12
|
+
import termios
|
|
13
|
+
import select
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
if not sys.stdin.isatty():
|
|
17
|
+
context['output'] = "Setup wizard requires an interactive terminal."
|
|
18
|
+
|
|
19
|
+
else:
|
|
20
|
+
def detect_ollama_models():
|
|
21
|
+
models = []
|
|
22
|
+
try:
|
|
23
|
+
import ollama
|
|
24
|
+
result = ollama.list()
|
|
25
|
+
for model in result.get('models', []):
|
|
26
|
+
name = model.get('model', model.get('name', ''))
|
|
27
|
+
if name:
|
|
28
|
+
models.append(('ollama', name))
|
|
29
|
+
except:
|
|
30
|
+
pass
|
|
31
|
+
return models
|
|
32
|
+
|
|
33
|
+
def detect_lm_studio_models():
|
|
34
|
+
models = []
|
|
35
|
+
try:
|
|
36
|
+
import requests
|
|
37
|
+
resp = requests.get('http://localhost:1234/v1/models', timeout=2)
|
|
38
|
+
if resp.status_code == 200:
|
|
39
|
+
data = resp.json()
|
|
40
|
+
for m in data.get('data', []):
|
|
41
|
+
models.append(('lm_studio', m.get('id', '')))
|
|
42
|
+
except:
|
|
43
|
+
pass
|
|
44
|
+
return models
|
|
45
|
+
|
|
46
|
+
def detect_api_keys():
|
|
47
|
+
keys = {}
|
|
48
|
+
for key in ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'DEEPSEEK_API_KEY']:
|
|
49
|
+
if os.environ.get(key):
|
|
50
|
+
keys[key] = True
|
|
51
|
+
return keys
|
|
52
|
+
|
|
53
|
+
def get_cloud_models(api_keys):
|
|
54
|
+
models = []
|
|
55
|
+
if api_keys.get('ANTHROPIC_API_KEY'):
|
|
56
|
+
models.extend([
|
|
57
|
+
('anthropic', 'claude-sonnet-4-20250514'),
|
|
58
|
+
('anthropic', 'claude-3-5-haiku-20241022'),
|
|
59
|
+
])
|
|
60
|
+
if api_keys.get('OPENAI_API_KEY'):
|
|
61
|
+
models.extend([
|
|
62
|
+
('openai', 'gpt-4o'),
|
|
63
|
+
('openai', 'gpt-4o-mini'),
|
|
64
|
+
])
|
|
65
|
+
if api_keys.get('GEMINI_API_KEY'):
|
|
66
|
+
models.extend([
|
|
67
|
+
('gemini', 'gemini-2.0-flash'),
|
|
68
|
+
('gemini', 'gemini-1.5-pro'),
|
|
69
|
+
])
|
|
70
|
+
if api_keys.get('DEEPSEEK_API_KEY'):
|
|
71
|
+
models.extend([
|
|
72
|
+
('deepseek', 'deepseek-chat'),
|
|
73
|
+
])
|
|
74
|
+
return models
|
|
75
|
+
|
|
76
|
+
class SetupState:
|
|
77
|
+
def __init__(self):
|
|
78
|
+
self.phase = 'detect'
|
|
79
|
+
self.local_models = []
|
|
80
|
+
self.cloud_models = []
|
|
81
|
+
self.api_keys = {}
|
|
82
|
+
self.providers_status = {}
|
|
83
|
+
self.selected_idx = 0
|
|
84
|
+
self.scroll_offset = 0
|
|
85
|
+
self.selections = {'chat_model': None, 'chat_provider': None}
|
|
86
|
+
self.status = "Detecting..."
|
|
87
|
+
|
|
88
|
+
state = SetupState()
|
|
89
|
+
|
|
90
|
+
def get_size():
|
|
91
|
+
try:
|
|
92
|
+
s = os.get_terminal_size()
|
|
93
|
+
return s.columns, s.lines
|
|
94
|
+
except:
|
|
95
|
+
return 80, 24
|
|
96
|
+
|
|
97
|
+
def get_all_models():
|
|
98
|
+
return state.local_models + state.cloud_models
|
|
99
|
+
|
|
100
|
+
def render_screen():
|
|
101
|
+
width, height = get_size()
|
|
102
|
+
out = []
|
|
103
|
+
out.append("\033[2J\033[H")
|
|
104
|
+
header = " NPCSH Setup Wizard "
|
|
105
|
+
out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
|
|
106
|
+
out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;1m{header}\033[0m")
|
|
107
|
+
|
|
108
|
+
if state.phase == 'detect':
|
|
109
|
+
out.append(f"\033[3;2H\033[1mDetected Providers:\033[0m")
|
|
110
|
+
row = 5
|
|
111
|
+
for provider, status in state.providers_status.items():
|
|
112
|
+
icon = "\033[32m✓\033[0m" if status['available'] else "\033[31m✗\033[0m"
|
|
113
|
+
count = f"({status['count']} models)" if status['count'] > 0 else "(not running)"
|
|
114
|
+
out.append(f"\033[{row};4H{icon} {provider} {count}")
|
|
115
|
+
row += 1
|
|
116
|
+
row += 1
|
|
117
|
+
out.append(f"\033[{row};2H\033[1mAPI Keys:\033[0m")
|
|
118
|
+
row += 1
|
|
119
|
+
for key in ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'DEEPSEEK_API_KEY']:
|
|
120
|
+
present = state.api_keys.get(key, False)
|
|
121
|
+
icon = "\033[32m✓\033[0m" if present else "\033[31m✗\033[0m"
|
|
122
|
+
short_key = key.replace('_API_KEY', '').lower()
|
|
123
|
+
out.append(f"\033[{row};4H{icon} {short_key}")
|
|
124
|
+
row += 1
|
|
125
|
+
row += 2
|
|
126
|
+
out.append(f"\033[{row};2H\033[33m{state.status}\033[0m")
|
|
127
|
+
row += 2
|
|
128
|
+
out.append(f"\033[{row};2HPress [Enter] to select model, [q] to quit")
|
|
129
|
+
|
|
130
|
+
elif state.phase == 'select_chat':
|
|
131
|
+
out.append(f"\033[3;2H\033[1mSelect Default Chat Model:\033[0m")
|
|
132
|
+
models = get_all_models()
|
|
133
|
+
visible_height = height - 8
|
|
134
|
+
visible = models[state.scroll_offset:state.scroll_offset + visible_height]
|
|
135
|
+
for i, (provider, model) in enumerate(visible):
|
|
136
|
+
row = 5 + i
|
|
137
|
+
idx = i + state.scroll_offset
|
|
138
|
+
if idx == state.selected_idx:
|
|
139
|
+
out.append(f"\033[{row};4H\033[47;30m> {model} ({provider})\033[0m")
|
|
140
|
+
else:
|
|
141
|
+
out.append(f"\033[{row};4H {model} \033[90m({provider})\033[0m")
|
|
142
|
+
|
|
143
|
+
out.append(f"\033[{height};1H\033[90m[j/k] Navigate [Enter] Select [q] Quit\033[0m")
|
|
144
|
+
sys.stdout.write(''.join(out))
|
|
145
|
+
sys.stdout.flush()
|
|
146
|
+
|
|
147
|
+
def handle_input(c):
|
|
148
|
+
if c == 'q':
|
|
149
|
+
return False
|
|
150
|
+
if c == '\x1b':
|
|
151
|
+
if select.select([sys.stdin], [], [], 0.05)[0]:
|
|
152
|
+
c2 = sys.stdin.read(1)
|
|
153
|
+
if c2 == '[':
|
|
154
|
+
c3 = sys.stdin.read(1)
|
|
155
|
+
if c3 == 'A' and state.phase == 'select_chat':
|
|
156
|
+
state.selected_idx = max(0, state.selected_idx - 1)
|
|
157
|
+
if state.selected_idx < state.scroll_offset:
|
|
158
|
+
state.scroll_offset = state.selected_idx
|
|
159
|
+
elif c3 == 'B' and state.phase == 'select_chat':
|
|
160
|
+
models = get_all_models()
|
|
161
|
+
_, height = get_size()
|
|
162
|
+
state.selected_idx = min(len(models) - 1, state.selected_idx + 1)
|
|
163
|
+
visible_height = height - 8
|
|
164
|
+
if state.selected_idx >= state.scroll_offset + visible_height:
|
|
165
|
+
state.scroll_offset = state.selected_idx - visible_height + 1
|
|
166
|
+
return True
|
|
167
|
+
if c == 'k' and state.phase == 'select_chat':
|
|
168
|
+
state.selected_idx = max(0, state.selected_idx - 1)
|
|
169
|
+
if state.selected_idx < state.scroll_offset:
|
|
170
|
+
state.scroll_offset = state.selected_idx
|
|
171
|
+
elif c == 'j' and state.phase == 'select_chat':
|
|
172
|
+
models = get_all_models()
|
|
173
|
+
_, height = get_size()
|
|
174
|
+
state.selected_idx = min(len(models) - 1, state.selected_idx + 1)
|
|
175
|
+
visible_height = height - 8
|
|
176
|
+
if state.selected_idx >= state.scroll_offset + visible_height:
|
|
177
|
+
state.scroll_offset = state.selected_idx - visible_height + 1
|
|
178
|
+
elif c == '\r' or c == '\n':
|
|
179
|
+
if state.phase == 'detect':
|
|
180
|
+
if get_all_models():
|
|
181
|
+
state.phase = 'select_chat'
|
|
182
|
+
state.selected_idx = 0
|
|
183
|
+
else:
|
|
184
|
+
return False
|
|
185
|
+
elif state.phase == 'select_chat':
|
|
186
|
+
models = get_all_models()
|
|
187
|
+
if models and state.selected_idx < len(models):
|
|
188
|
+
provider, model = models[state.selected_idx]
|
|
189
|
+
state.selections['chat_model'] = model
|
|
190
|
+
state.selections['chat_provider'] = provider
|
|
191
|
+
from npcsh.config import set_npcsh_config_value
|
|
192
|
+
set_npcsh_config_value('NPCSH_CHAT_MODEL', model)
|
|
193
|
+
set_npcsh_config_value('NPCSH_CHAT_PROVIDER', provider)
|
|
194
|
+
return False
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
def run_detection():
|
|
198
|
+
state.status = "Detecting Ollama..."
|
|
199
|
+
render_screen()
|
|
200
|
+
ollama_models = detect_ollama_models()
|
|
201
|
+
state.providers_status['ollama'] = {'available': len(ollama_models) > 0, 'count': len(ollama_models)}
|
|
202
|
+
state.local_models.extend(ollama_models)
|
|
203
|
+
|
|
204
|
+
state.status = "Detecting LM Studio..."
|
|
205
|
+
render_screen()
|
|
206
|
+
lm_models = detect_lm_studio_models()
|
|
207
|
+
state.providers_status['lm_studio'] = {'available': len(lm_models) > 0, 'count': len(lm_models)}
|
|
208
|
+
state.local_models.extend(lm_models)
|
|
209
|
+
|
|
210
|
+
state.status = "Checking API keys..."
|
|
211
|
+
render_screen()
|
|
212
|
+
state.api_keys = detect_api_keys()
|
|
213
|
+
state.cloud_models = get_cloud_models(state.api_keys)
|
|
214
|
+
|
|
215
|
+
total = len(state.local_models) + len(state.cloud_models)
|
|
216
|
+
state.status = f"Found {total} models."
|
|
217
|
+
|
|
218
|
+
fd = sys.stdin.fileno()
|
|
219
|
+
old_settings = termios.tcgetattr(fd)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
tty.setcbreak(fd)
|
|
223
|
+
sys.stdout.write('\033[?25l')
|
|
224
|
+
run_detection()
|
|
225
|
+
render_screen()
|
|
226
|
+
while True:
|
|
227
|
+
c = sys.stdin.read(1)
|
|
228
|
+
if not handle_input(c):
|
|
229
|
+
break
|
|
230
|
+
render_screen()
|
|
231
|
+
finally:
|
|
232
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
233
|
+
sys.stdout.write('\033[?25h')
|
|
234
|
+
sys.stdout.write('\033[2J\033[H')
|
|
235
|
+
sys.stdout.flush()
|
|
236
|
+
|
|
237
|
+
if state.selections['chat_model']:
|
|
238
|
+
context['output'] = f"Setup complete! Model: {state.selections['chat_model']} ({state.selections['chat_provider']})"
|
|
239
|
+
else:
|
|
240
|
+
context['output'] = "Setup cancelled."
|
|
@@ -42,44 +42,6 @@ steps:
|
|
|
42
42
|
# If none specified, sync all
|
|
43
43
|
sync_all = not (sync_jinxs or sync_npcs or sync_ctx or sync_images)
|
|
44
44
|
|
|
45
|
-
# Find the repo npc_team directory
|
|
46
|
-
# Check common locations
|
|
47
|
-
possible_repo_paths = [
|
|
48
|
-
Path.home() / "npcww" / "npc-core" / "npcsh" / "npcsh" / "npc_team",
|
|
49
|
-
Path.home() / "npc-core" / "npcsh" / "npcsh" / "npc_team",
|
|
50
|
-
Path.home() / "repos" / "npcsh" / "npcsh" / "npc_team",
|
|
51
|
-
Path.home() / "Projects" / "npcsh" / "npcsh" / "npc_team",
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
# Also check if we can find it via pip show
|
|
55
|
-
try:
|
|
56
|
-
import subprocess
|
|
57
|
-
result = subprocess.run(['pip', 'show', 'npcsh', '-f'], capture_output=True, text=True)
|
|
58
|
-
if result.returncode == 0:
|
|
59
|
-
for line in result.stdout.split('\n'):
|
|
60
|
-
if line.startswith('Location:'):
|
|
61
|
-
pip_path = Path(line.split(':', 1)[1].strip()) / "npcsh" / "npc_team"
|
|
62
|
-
if pip_path.exists():
|
|
63
|
-
possible_repo_paths.insert(0, pip_path)
|
|
64
|
-
except:
|
|
65
|
-
pass
|
|
66
|
-
|
|
67
|
-
repo_npc_team = None
|
|
68
|
-
for path in possible_repo_paths:
|
|
69
|
-
if path.exists() and path.is_dir():
|
|
70
|
-
repo_npc_team = path
|
|
71
|
-
break
|
|
72
|
-
|
|
73
|
-
local_npc_team = Path.home() / ".npcsh" / "npc_team"
|
|
74
|
-
|
|
75
|
-
if not repo_npc_team:
|
|
76
|
-
context['output'] = "Error: Could not find npcsh repo npc_team directory.\nSearched:\n" + "\n".join(f" - {p}" for p in possible_repo_paths)
|
|
77
|
-
exit()
|
|
78
|
-
|
|
79
|
-
if not local_npc_team.exists():
|
|
80
|
-
context['output'] = f"Error: Local npc_team directory not found at {local_npc_team}"
|
|
81
|
-
exit()
|
|
82
|
-
|
|
83
45
|
def get_file_hash(filepath):
|
|
84
46
|
"""Get MD5 hash of file contents."""
|
|
85
47
|
try:
|
|
@@ -104,127 +66,158 @@ steps:
|
|
|
104
66
|
files.append(rel_path)
|
|
105
67
|
return files
|
|
106
68
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if sync_all or sync_ctx:
|
|
112
|
-
sync_extensions.append('.ctx')
|
|
113
|
-
if sync_all or sync_jinxs:
|
|
114
|
-
sync_extensions.append('.jinx')
|
|
115
|
-
if sync_all or sync_images:
|
|
116
|
-
sync_extensions.extend(['.png', '.jpg', '.jpeg'])
|
|
117
|
-
|
|
118
|
-
# Get files from repo
|
|
119
|
-
repo_files = get_files_recursive(repo_npc_team, sync_extensions)
|
|
120
|
-
|
|
121
|
-
output_lines = []
|
|
122
|
-
output_lines.append(f"Syncing from: {repo_npc_team}")
|
|
123
|
-
output_lines.append(f"Syncing to: {local_npc_team}")
|
|
124
|
-
|
|
125
|
-
# Show what's being synced
|
|
126
|
-
sync_types = []
|
|
127
|
-
if sync_all:
|
|
128
|
-
sync_types.append("all")
|
|
129
|
-
else:
|
|
130
|
-
if sync_npcs: sync_types.append("npcs")
|
|
131
|
-
if sync_ctx: sync_types.append("ctx")
|
|
132
|
-
if sync_jinxs: sync_types.append("jinxs")
|
|
133
|
-
if sync_images: sync_types.append("images")
|
|
134
|
-
output_lines.append(f"Syncing: {', '.join(sync_types)}")
|
|
135
|
-
|
|
136
|
-
if dry_run:
|
|
137
|
-
output_lines.append("\n[DRY RUN - No changes will be made]\n")
|
|
138
|
-
output_lines.append("")
|
|
139
|
-
|
|
140
|
-
new_files = []
|
|
141
|
-
updated_files = []
|
|
142
|
-
modified_locally = []
|
|
143
|
-
unchanged_files = []
|
|
144
|
-
|
|
145
|
-
for rel_path in repo_files:
|
|
146
|
-
repo_file = repo_npc_team / rel_path
|
|
147
|
-
local_file = local_npc_team / rel_path
|
|
148
|
-
|
|
149
|
-
if not local_file.exists():
|
|
150
|
-
new_files.append(rel_path)
|
|
151
|
-
else:
|
|
152
|
-
repo_hash = get_file_hash(repo_file)
|
|
153
|
-
local_hash = get_file_hash(local_file)
|
|
154
|
-
|
|
155
|
-
if repo_hash == local_hash:
|
|
156
|
-
unchanged_files.append(rel_path)
|
|
157
|
-
else:
|
|
158
|
-
# Check if local file is newer (possibly modified by user)
|
|
159
|
-
repo_mtime = repo_file.stat().st_mtime
|
|
160
|
-
local_mtime = local_file.stat().st_mtime
|
|
161
|
-
|
|
162
|
-
if local_mtime > repo_mtime:
|
|
163
|
-
modified_locally.append((rel_path, local_mtime, repo_mtime))
|
|
164
|
-
else:
|
|
165
|
-
updated_files.append(rel_path)
|
|
69
|
+
def do_sync():
|
|
70
|
+
# Find the npc_team directory from the installed npcsh package
|
|
71
|
+
import subprocess
|
|
72
|
+
repo_npc_team = None
|
|
166
73
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
74
|
+
result = subprocess.run(['pip', 'show', 'npcsh'], capture_output=True, text=True)
|
|
75
|
+
if result.returncode == 0:
|
|
76
|
+
location = None
|
|
77
|
+
editable_location = None
|
|
78
|
+
for line in result.stdout.split('\n'):
|
|
79
|
+
if line.startswith('Location:'):
|
|
80
|
+
location = Path(line.split(':', 1)[1].strip())
|
|
81
|
+
elif line.startswith('Editable project location:'):
|
|
82
|
+
editable_location = Path(line.split(':', 1)[1].strip())
|
|
83
|
+
|
|
84
|
+
# Prefer editable location if available
|
|
85
|
+
if editable_location:
|
|
86
|
+
repo_npc_team = editable_location / "npcsh" / "npc_team"
|
|
87
|
+
elif location:
|
|
88
|
+
repo_npc_team = location / "npcsh" / "npc_team"
|
|
89
|
+
|
|
90
|
+
if not repo_npc_team or not repo_npc_team.exists():
|
|
91
|
+
return f"Error: Could not find npcsh package npc_team directory. Is npcsh installed?"
|
|
92
|
+
|
|
93
|
+
local_npc_team = Path.home() / ".npcsh" / "npc_team"
|
|
94
|
+
|
|
95
|
+
if not local_npc_team.exists():
|
|
96
|
+
return f"Error: Local npc_team directory not found at {local_npc_team}"
|
|
97
|
+
|
|
98
|
+
# Build list of extensions to sync based on flags
|
|
99
|
+
sync_extensions = []
|
|
100
|
+
if sync_all or sync_npcs:
|
|
101
|
+
sync_extensions.append('.npc')
|
|
102
|
+
if sync_all or sync_ctx:
|
|
103
|
+
sync_extensions.append('.ctx')
|
|
104
|
+
if sync_all or sync_jinxs:
|
|
105
|
+
sync_extensions.append('.jinx')
|
|
106
|
+
if sync_all or sync_images:
|
|
107
|
+
sync_extensions.extend(['.png', '.jpg', '.jpeg'])
|
|
108
|
+
|
|
109
|
+
# Get files from repo
|
|
110
|
+
repo_files = get_files_recursive(repo_npc_team, sync_extensions)
|
|
111
|
+
|
|
112
|
+
output_lines = []
|
|
113
|
+
output_lines.append(f"Syncing from: {repo_npc_team}")
|
|
114
|
+
output_lines.append(f"Syncing to: {local_npc_team}")
|
|
115
|
+
|
|
116
|
+
# Show what's being synced
|
|
117
|
+
sync_types = []
|
|
118
|
+
if sync_all:
|
|
119
|
+
sync_types.append("all")
|
|
120
|
+
else:
|
|
121
|
+
if sync_npcs: sync_types.append("npcs")
|
|
122
|
+
if sync_ctx: sync_types.append("ctx")
|
|
123
|
+
if sync_jinxs: sync_types.append("jinxs")
|
|
124
|
+
if sync_images: sync_types.append("images")
|
|
125
|
+
output_lines.append(f"Syncing: {', '.join(sync_types)}")
|
|
126
|
+
|
|
127
|
+
if dry_run:
|
|
128
|
+
output_lines.append("\n[DRY RUN - No changes will be made]\n")
|
|
172
129
|
output_lines.append("")
|
|
173
130
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
output_lines.append("")
|
|
131
|
+
new_files = []
|
|
132
|
+
updated_files = []
|
|
133
|
+
modified_locally = []
|
|
134
|
+
unchanged_files = []
|
|
179
135
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
local_dt = datetime.fromtimestamp(local_t).strftime('%Y-%m-%d %H:%M')
|
|
184
|
-
repo_dt = datetime.fromtimestamp(repo_t).strftime('%Y-%m-%d %H:%M')
|
|
185
|
-
output_lines.append(f" ! {f}")
|
|
186
|
-
output_lines.append(f" local: {local_dt} repo: {repo_dt}")
|
|
187
|
-
if not force:
|
|
188
|
-
output_lines.append(" (use --force to overwrite these)")
|
|
189
|
-
output_lines.append("")
|
|
136
|
+
for rel_path in repo_files:
|
|
137
|
+
repo_file = repo_npc_team / rel_path
|
|
138
|
+
local_file = local_npc_team / rel_path
|
|
190
139
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
140
|
+
if not local_file.exists():
|
|
141
|
+
new_files.append(rel_path)
|
|
142
|
+
else:
|
|
143
|
+
repo_hash = get_file_hash(repo_file)
|
|
144
|
+
local_hash = get_file_hash(local_file)
|
|
194
145
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
146
|
+
if repo_hash == local_hash:
|
|
147
|
+
unchanged_files.append(rel_path)
|
|
148
|
+
else:
|
|
149
|
+
# Check if local file is newer (possibly modified by user)
|
|
150
|
+
repo_mtime = repo_file.stat().st_mtime
|
|
151
|
+
local_mtime = local_file.stat().st_mtime
|
|
152
|
+
|
|
153
|
+
if local_mtime > repo_mtime:
|
|
154
|
+
modified_locally.append((rel_path, local_mtime, repo_mtime))
|
|
155
|
+
else:
|
|
156
|
+
updated_files.append(rel_path)
|
|
157
|
+
|
|
158
|
+
# Report findings
|
|
159
|
+
if new_files:
|
|
160
|
+
output_lines.append(f"New files to add ({len(new_files)}):")
|
|
161
|
+
for f in new_files:
|
|
162
|
+
output_lines.append(f" + {f}")
|
|
163
|
+
output_lines.append("")
|
|
164
|
+
|
|
165
|
+
if updated_files:
|
|
166
|
+
output_lines.append(f"Files to update ({len(updated_files)}):")
|
|
167
|
+
for f in updated_files:
|
|
168
|
+
output_lines.append(f" ~ {f}")
|
|
169
|
+
output_lines.append("")
|
|
170
|
+
|
|
171
|
+
if modified_locally:
|
|
172
|
+
output_lines.append(f"Locally modified files ({len(modified_locally)}):")
|
|
173
|
+
for f, local_t, repo_t in modified_locally:
|
|
174
|
+
local_dt = datetime.fromtimestamp(local_t).strftime('%Y-%m-%d %H:%M')
|
|
175
|
+
repo_dt = datetime.fromtimestamp(repo_t).strftime('%Y-%m-%d %H:%M')
|
|
176
|
+
output_lines.append(f" ! {f}")
|
|
177
|
+
output_lines.append(f" local: {local_dt} repo: {repo_dt}")
|
|
178
|
+
if not force:
|
|
179
|
+
output_lines.append(" (use --force to overwrite these)")
|
|
180
|
+
output_lines.append("")
|
|
181
|
+
|
|
182
|
+
if unchanged_files:
|
|
183
|
+
output_lines.append(f"Already up to date: {len(unchanged_files)} files")
|
|
184
|
+
output_lines.append("")
|
|
185
|
+
|
|
186
|
+
# Perform sync if not dry run
|
|
187
|
+
if not dry_run:
|
|
188
|
+
synced = 0
|
|
189
|
+
skipped = 0
|
|
190
|
+
|
|
191
|
+
# Sync new files
|
|
192
|
+
for rel_path in new_files:
|
|
219
193
|
src = repo_npc_team / rel_path
|
|
220
194
|
dst = local_npc_team / rel_path
|
|
195
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
221
196
|
shutil.copy2(src, dst)
|
|
222
197
|
synced += 1
|
|
223
|
-
else:
|
|
224
|
-
skipped += 1
|
|
225
198
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
199
|
+
# Sync updated files
|
|
200
|
+
for rel_path in updated_files:
|
|
201
|
+
src = repo_npc_team / rel_path
|
|
202
|
+
dst = local_npc_team / rel_path
|
|
203
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
shutil.copy2(src, dst)
|
|
205
|
+
synced += 1
|
|
206
|
+
|
|
207
|
+
# Handle locally modified files
|
|
208
|
+
for rel_path, _, _ in modified_locally:
|
|
209
|
+
if force:
|
|
210
|
+
src = repo_npc_team / rel_path
|
|
211
|
+
dst = local_npc_team / rel_path
|
|
212
|
+
shutil.copy2(src, dst)
|
|
213
|
+
synced += 1
|
|
214
|
+
else:
|
|
215
|
+
skipped += 1
|
|
216
|
+
|
|
217
|
+
output_lines.append(f"Synced: {synced} files")
|
|
218
|
+
if skipped:
|
|
219
|
+
output_lines.append(f"Skipped: {skipped} locally modified files")
|
|
220
|
+
|
|
221
|
+
return "\n".join(output_lines)
|
|
229
222
|
|
|
230
|
-
context['output'] =
|
|
223
|
+
context['output'] = do_sync()
|