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.
Files changed (139) hide show
  1. npcsh/_state.py +8 -0
  2. npcsh/benchmark/npcsh_agent.py +47 -16
  3. npcsh/config.py +1 -0
  4. npcsh/diff_viewer.py +452 -0
  5. npcsh/npc_team/jinxs/bin/config_tui.jinx +299 -0
  6. npcsh/npc_team/jinxs/bin/memories.jinx +316 -0
  7. npcsh/npc_team/jinxs/bin/setup.jinx +240 -0
  8. npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
  9. npcsh/npc_team/jinxs/bin/team_tui.jinx +327 -0
  10. npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
  11. npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
  12. npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
  13. npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
  14. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
  15. npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
  16. npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
  17. npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
  18. npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
  19. npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
  20. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
  21. npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
  22. npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
  23. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
  24. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
  25. npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
  26. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
  27. npcsh/npc_team/jinxs/modes/guac.jinx +0 -2
  28. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
  29. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
  30. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
  31. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +299 -0
  32. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
  33. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
  34. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.jinx +0 -2
  35. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
  36. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
  37. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
  38. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
  39. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
  40. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
  41. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
  42. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/send_message.jinx +1 -1
  43. npcsh-1.1.19.data/data/npcsh/npc_team/setup.jinx +240 -0
  44. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
  45. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
  46. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
  47. npcsh-1.1.19.data/data/npcsh/npc_team/sync.jinx +223 -0
  48. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +327 -0
  49. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
  50. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
  51. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/METADATA +21 -14
  52. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/RECORD +138 -129
  53. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -0
  54. npcsh-1.1.18.data/data/npcsh/npc_team/sync.jinx +0 -230
  55. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
  56. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  57. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
  58. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  59. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  60. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  61. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  62. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
  63. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
  64. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
  65. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  66. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  67. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
  68. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
  69. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
  70. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.jinx +0 -0
  71. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
  72. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
  73. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
  74. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  75. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  76. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  77. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  78. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
  79. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
  80. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
  81. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
  82. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
  83. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  84. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
  85. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
  86. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  87. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  88. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  89. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kg_search.jinx +0 -0
  90. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  91. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  92. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
  93. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/mem_search.jinx +0 -0
  94. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  95. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  96. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
  97. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  98. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
  99. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paper_search.jinx +0 -0
  100. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
  101. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.jinx +0 -0
  102. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
  103. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
  104. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  105. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  106. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/pti.jinx +0 -0
  107. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
  108. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  109. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
  110. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
  111. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  112. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
  113. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/semantic_scholar.jinx +0 -0
  114. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
  115. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
  116. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
  117. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
  118. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  119. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
  120. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  121. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.jinx +0 -0
  122. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
  123. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sql.jinx +0 -0
  124. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
  125. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
  126. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  127. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  128. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  129. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
  130. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  131. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  132. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
  133. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wander.jinx +0 -0
  134. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  135. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +0 -0
  136. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
  137. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/WHEEL +0 -0
  138. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
  139. {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
- # Build list of extensions to sync based on flags
108
- sync_extensions = []
109
- if sync_all or sync_npcs:
110
- sync_extensions.append('.npc')
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
- # Report findings
168
- if new_files:
169
- output_lines.append(f"New files to add ({len(new_files)}):")
170
- for f in new_files:
171
- output_lines.append(f" + {f}")
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
- if updated_files:
175
- output_lines.append(f"Files to update ({len(updated_files)}):")
176
- for f in updated_files:
177
- output_lines.append(f" ~ {f}")
178
- output_lines.append("")
131
+ new_files = []
132
+ updated_files = []
133
+ modified_locally = []
134
+ unchanged_files = []
179
135
 
180
- if modified_locally:
181
- output_lines.append(f"Locally modified files ({len(modified_locally)}):")
182
- for f, local_t, repo_t in modified_locally:
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
- if unchanged_files:
192
- output_lines.append(f"Already up to date: {len(unchanged_files)} files")
193
- output_lines.append("")
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
- # Perform sync if not dry run
196
- if not dry_run:
197
- synced = 0
198
- skipped = 0
199
-
200
- # Sync new files
201
- for rel_path in new_files:
202
- src = repo_npc_team / rel_path
203
- dst = local_npc_team / rel_path
204
- dst.parent.mkdir(parents=True, exist_ok=True)
205
- shutil.copy2(src, dst)
206
- synced += 1
207
-
208
- # Sync updated files
209
- for rel_path in updated_files:
210
- src = repo_npc_team / rel_path
211
- dst = local_npc_team / rel_path
212
- dst.parent.mkdir(parents=True, exist_ok=True)
213
- shutil.copy2(src, dst)
214
- synced += 1
215
-
216
- # Handle locally modified files
217
- for rel_path, _, _ in modified_locally:
218
- if force:
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
- output_lines.append(f"Synced: {synced} files")
227
- if skipped:
228
- output_lines.append(f"Skipped: {skipped} locally modified files")
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'] = "\n".join(output_lines)
223
+ context['output'] = do_sync()