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."
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
jinx_name: "sync"
|
|
2
|
+
description: "Sync npc_team files from the npcsh repo to ~/.npcsh/npc_team. Detects local modifications before overwriting."
|
|
3
|
+
inputs:
|
|
4
|
+
- force: ""
|
|
5
|
+
- dry_run: ""
|
|
6
|
+
- jinxs: ""
|
|
7
|
+
- npcs: ""
|
|
8
|
+
- ctx: ""
|
|
9
|
+
- images: ""
|
|
10
|
+
steps:
|
|
11
|
+
- name: "sync_npc_team"
|
|
12
|
+
engine: "python"
|
|
13
|
+
code: |
|
|
14
|
+
import os
|
|
15
|
+
import hashlib
|
|
16
|
+
import shutil
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
force = context.get('force', False)
|
|
21
|
+
dry_run = context.get('dry_run', False)
|
|
22
|
+
sync_jinxs = context.get('jinxs', False)
|
|
23
|
+
sync_npcs = context.get('npcs', False)
|
|
24
|
+
sync_ctx = context.get('ctx', False)
|
|
25
|
+
sync_images = context.get('images', False)
|
|
26
|
+
|
|
27
|
+
# Convert string flags to boolean
|
|
28
|
+
def to_bool(val):
|
|
29
|
+
if isinstance(val, bool):
|
|
30
|
+
return val
|
|
31
|
+
if isinstance(val, str):
|
|
32
|
+
return val.lower() in ('true', '1', 'yes', 'y')
|
|
33
|
+
return bool(val)
|
|
34
|
+
|
|
35
|
+
force = to_bool(force)
|
|
36
|
+
dry_run = to_bool(dry_run)
|
|
37
|
+
sync_jinxs = to_bool(sync_jinxs)
|
|
38
|
+
sync_npcs = to_bool(sync_npcs)
|
|
39
|
+
sync_ctx = to_bool(sync_ctx)
|
|
40
|
+
sync_images = to_bool(sync_images)
|
|
41
|
+
|
|
42
|
+
# If none specified, sync all
|
|
43
|
+
sync_all = not (sync_jinxs or sync_npcs or sync_ctx or sync_images)
|
|
44
|
+
|
|
45
|
+
def get_file_hash(filepath):
|
|
46
|
+
"""Get MD5 hash of file contents."""
|
|
47
|
+
try:
|
|
48
|
+
with open(filepath, 'rb') as f:
|
|
49
|
+
return hashlib.md5(f.read()).hexdigest()
|
|
50
|
+
except:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def get_files_recursive(base_path, extensions=None):
|
|
54
|
+
"""Get all files recursively, optionally filtered by extensions."""
|
|
55
|
+
files = []
|
|
56
|
+
for root, dirs, filenames in os.walk(base_path):
|
|
57
|
+
# Skip .git directories
|
|
58
|
+
dirs[:] = [d for d in dirs if d != '.git']
|
|
59
|
+
for filename in filenames:
|
|
60
|
+
if filename.startswith('.'):
|
|
61
|
+
continue
|
|
62
|
+
if extensions and not any(filename.endswith(ext) for ext in extensions):
|
|
63
|
+
continue
|
|
64
|
+
full_path = Path(root) / filename
|
|
65
|
+
rel_path = full_path.relative_to(base_path)
|
|
66
|
+
files.append(rel_path)
|
|
67
|
+
return files
|
|
68
|
+
|
|
69
|
+
def do_sync():
|
|
70
|
+
# Find the npc_team directory from the installed npcsh package
|
|
71
|
+
import subprocess
|
|
72
|
+
repo_npc_team = None
|
|
73
|
+
|
|
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")
|
|
129
|
+
output_lines.append("")
|
|
130
|
+
|
|
131
|
+
new_files = []
|
|
132
|
+
updated_files = []
|
|
133
|
+
modified_locally = []
|
|
134
|
+
unchanged_files = []
|
|
135
|
+
|
|
136
|
+
for rel_path in repo_files:
|
|
137
|
+
repo_file = repo_npc_team / rel_path
|
|
138
|
+
local_file = local_npc_team / rel_path
|
|
139
|
+
|
|
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)
|
|
145
|
+
|
|
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:
|
|
193
|
+
src = repo_npc_team / rel_path
|
|
194
|
+
dst = local_npc_team / rel_path
|
|
195
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
shutil.copy2(src, dst)
|
|
197
|
+
synced += 1
|
|
198
|
+
|
|
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)
|
|
222
|
+
|
|
223
|
+
context['output'] = do_sync()
|