npcsh 1.1.18__py3-none-any.whl → 1.1.20__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 (165) hide show
  1. npcsh/_state.py +19 -7
  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 +300 -0
  6. npcsh/npc_team/jinxs/bin/jinxs.jinx +407 -0
  7. npcsh/npc_team/jinxs/bin/kg.jinx +941 -0
  8. npcsh/npc_team/jinxs/bin/memories.jinx +317 -0
  9. npcsh/npc_team/jinxs/bin/models.jinx +343 -0
  10. npcsh/npc_team/jinxs/bin/nql.jinx +380 -50
  11. npcsh/npc_team/jinxs/bin/setup.jinx +241 -0
  12. npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
  13. npcsh/npc_team/jinxs/bin/team.jinx +504 -0
  14. npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
  15. npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
  16. npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
  17. npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
  18. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
  19. npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
  20. npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
  21. npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
  22. npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
  23. npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
  24. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
  25. npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
  26. npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
  27. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
  28. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
  29. npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
  30. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
  31. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +1 -1
  32. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +1 -1
  33. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +1 -1
  34. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +1 -1
  35. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +1 -1
  36. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +1 -1
  37. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +1 -1
  38. npcsh/npc_team/jinxs/modes/alicanto.jinx +1 -1
  39. npcsh/npc_team/jinxs/modes/arxiv.jinx +1 -1
  40. npcsh/npc_team/jinxs/modes/corca.jinx +1 -1
  41. npcsh/npc_team/jinxs/modes/guac.jinx +4 -6
  42. npcsh/npc_team/jinxs/modes/plonk.jinx +1 -1
  43. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  44. npcsh/npc_team/jinxs/modes/reattach.jinx +1 -1
  45. npcsh/npc_team/jinxs/modes/spool.jinx +1 -1
  46. npcsh/npc_team/jinxs/modes/wander.jinx +1 -1
  47. npcsh/routes.py +8 -2
  48. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
  49. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.jinx +1 -1
  50. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/arxiv.jinx +1 -1
  51. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
  52. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
  53. npcsh-1.1.20.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  54. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/confirm.jinx +1 -1
  55. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.jinx +1 -1
  56. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/db_search.jinx +1 -1
  57. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/file_search.jinx +1 -1
  58. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
  59. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.jinx +4 -6
  60. npcsh-1.1.20.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  61. npcsh-1.1.20.data/data/npcsh/npc_team/kg.jinx +941 -0
  62. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kg_search.jinx +1 -1
  63. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
  64. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_search.jinx +1 -1
  65. npcsh-1.1.20.data/data/npcsh/npc_team/memories.jinx +317 -0
  66. npcsh-1.1.20.data/data/npcsh/npc_team/models.jinx +343 -0
  67. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/navigate.jinx +1 -1
  68. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/notify.jinx +1 -1
  69. npcsh-1.1.20.data/data/npcsh/npc_team/nql.jinx +471 -0
  70. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
  71. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paper_search.jinx +1 -1
  72. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.jinx +1 -1
  73. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/pti.jinx +1 -1
  74. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
  75. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/reattach.jinx +1 -1
  76. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
  77. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/semantic_scholar.jinx +1 -1
  78. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/send_message.jinx +1 -1
  79. npcsh-1.1.20.data/data/npcsh/npc_team/setup.jinx +241 -0
  80. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
  81. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.jinx +1 -1
  82. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
  83. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
  84. npcsh-1.1.20.data/data/npcsh/npc_team/sync.jinx +223 -0
  85. npcsh-1.1.20.data/data/npcsh/npc_team/team.jinx +504 -0
  86. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wander.jinx +1 -1
  87. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/web_search.jinx +1 -1
  88. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/write_file.jinx +1 -1
  89. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
  90. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/METADATA +21 -14
  91. npcsh-1.1.20.dist-info/RECORD +248 -0
  92. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/entry_points.txt +7 -0
  93. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -331
  94. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  95. npcsh-1.1.18.data/data/npcsh/npc_team/nql.jinx +0 -141
  96. npcsh-1.1.18.data/data/npcsh/npc_team/sync.jinx +0 -230
  97. npcsh-1.1.18.dist-info/RECORD +0 -235
  98. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  99. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/alicanto.png +0 -0
  100. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  101. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  102. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  103. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/build.jinx +0 -0
  104. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/chat.jinx +0 -0
  105. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/click.jinx +0 -0
  106. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  107. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  108. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compile.jinx +0 -0
  109. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/compress.jinx +0 -0
  110. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/convene.jinx +0 -0
  111. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.npc +0 -0
  112. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca.png +0 -0
  113. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/corca_example.png +0 -0
  114. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  115. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  116. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic.npc +0 -0
  117. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/frederic4.png +0 -0
  118. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.npc +0 -0
  119. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/guac.png +0 -0
  120. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/help.jinx +0 -0
  121. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  122. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/init.jinx +0 -0
  123. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  124. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  125. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  126. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  127. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  128. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
  129. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  130. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  131. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  132. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/ots.jinx +0 -0
  133. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/paste.jinx +0 -0
  134. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.npc +0 -0
  135. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonk.png +0 -0
  136. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  137. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  138. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/python.jinx +0 -0
  139. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sample.jinx +0 -0
  141. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  142. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/search.jinx +0 -0
  143. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/serve.jinx +0 -0
  144. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/set.jinx +0 -0
  145. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sh.jinx +0 -0
  146. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/shh.jinx +0 -0
  147. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  148. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sibiji.png +0 -0
  149. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  150. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/spool.png +0 -0
  151. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/sql.jinx +0 -0
  152. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switch.jinx +0 -0
  153. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/switches.jinx +0 -0
  154. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  155. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  156. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  157. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/usage.jinx +0 -0
  158. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  159. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  160. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/wait.jinx +0 -0
  161. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.jinx +0 -0
  162. {npcsh-1.1.18.data → npcsh-1.1.20.data}/data/npcsh/npc_team/yap.png +0 -0
  163. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/WHEEL +0 -0
  164. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/licenses/LICENSE +0 -0
  165. {npcsh-1.1.18.dist-info → npcsh-1.1.20.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,241 @@
1
+ jinx_name: setup
2
+ description: Interactive setup wizard for npcsh - detect local models, configure defaults
3
+ interactive: true
4
+ inputs:
5
+ - skip_detection: ""
6
+ steps:
7
+ - name: setup_wizard
8
+ engine: python
9
+ code: |
10
+ import os
11
+ import sys
12
+ import tty
13
+ import termios
14
+ import select
15
+ from pathlib import Path
16
+
17
+ if not sys.stdin.isatty():
18
+ context['output'] = "Setup wizard requires an interactive terminal."
19
+
20
+ else:
21
+ def detect_ollama_models():
22
+ models = []
23
+ try:
24
+ import ollama
25
+ result = ollama.list()
26
+ for model in result.get('models', []):
27
+ name = model.get('model', model.get('name', ''))
28
+ if name:
29
+ models.append(('ollama', name))
30
+ except:
31
+ pass
32
+ return models
33
+
34
+ def detect_lm_studio_models():
35
+ models = []
36
+ try:
37
+ import requests
38
+ resp = requests.get('http://localhost:1234/v1/models', timeout=2)
39
+ if resp.status_code == 200:
40
+ data = resp.json()
41
+ for m in data.get('data', []):
42
+ models.append(('lm_studio', m.get('id', '')))
43
+ except:
44
+ pass
45
+ return models
46
+
47
+ def detect_api_keys():
48
+ keys = {}
49
+ for key in ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'DEEPSEEK_API_KEY']:
50
+ if os.environ.get(key):
51
+ keys[key] = True
52
+ return keys
53
+
54
+ def get_cloud_models(api_keys):
55
+ models = []
56
+ if api_keys.get('ANTHROPIC_API_KEY'):
57
+ models.extend([
58
+ ('anthropic', 'claude-sonnet-4-20250514'),
59
+ ('anthropic', 'claude-3-5-haiku-20241022'),
60
+ ])
61
+ if api_keys.get('OPENAI_API_KEY'):
62
+ models.extend([
63
+ ('openai', 'gpt-4o'),
64
+ ('openai', 'gpt-4o-mini'),
65
+ ])
66
+ if api_keys.get('GEMINI_API_KEY'):
67
+ models.extend([
68
+ ('gemini', 'gemini-2.0-flash'),
69
+ ('gemini', 'gemini-1.5-pro'),
70
+ ])
71
+ if api_keys.get('DEEPSEEK_API_KEY'):
72
+ models.extend([
73
+ ('deepseek', 'deepseek-chat'),
74
+ ])
75
+ return models
76
+
77
+ class SetupState:
78
+ def __init__(self):
79
+ self.phase = 'detect'
80
+ self.local_models = []
81
+ self.cloud_models = []
82
+ self.api_keys = {}
83
+ self.providers_status = {}
84
+ self.selected_idx = 0
85
+ self.scroll_offset = 0
86
+ self.selections = {'chat_model': None, 'chat_provider': None}
87
+ self.status = "Detecting..."
88
+
89
+ state = SetupState()
90
+
91
+ def get_size():
92
+ try:
93
+ s = os.get_terminal_size()
94
+ return s.columns, s.lines
95
+ except:
96
+ return 80, 24
97
+
98
+ def get_all_models():
99
+ return state.local_models + state.cloud_models
100
+
101
+ def render_screen():
102
+ width, height = get_size()
103
+ out = []
104
+ out.append("\033[2J\033[H")
105
+ header = " NPCSH Setup Wizard "
106
+ out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
107
+ out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;1m{header}\033[0m")
108
+
109
+ if state.phase == 'detect':
110
+ out.append(f"\033[3;2H\033[1mDetected Providers:\033[0m")
111
+ row = 5
112
+ for provider, status in state.providers_status.items():
113
+ icon = "\033[32m✓\033[0m" if status['available'] else "\033[31m✗\033[0m"
114
+ count = f"({status['count']} models)" if status['count'] > 0 else "(not running)"
115
+ out.append(f"\033[{row};4H{icon} {provider} {count}")
116
+ row += 1
117
+ row += 1
118
+ out.append(f"\033[{row};2H\033[1mAPI Keys:\033[0m")
119
+ row += 1
120
+ for key in ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'DEEPSEEK_API_KEY']:
121
+ present = state.api_keys.get(key, False)
122
+ icon = "\033[32m✓\033[0m" if present else "\033[31m✗\033[0m"
123
+ short_key = key.replace('_API_KEY', '').lower()
124
+ out.append(f"\033[{row};4H{icon} {short_key}")
125
+ row += 1
126
+ row += 2
127
+ out.append(f"\033[{row};2H\033[33m{state.status}\033[0m")
128
+ row += 2
129
+ out.append(f"\033[{row};2HPress [Enter] to select model, [q] to quit")
130
+
131
+ elif state.phase == 'select_chat':
132
+ out.append(f"\033[3;2H\033[1mSelect Default Chat Model:\033[0m")
133
+ models = get_all_models()
134
+ visible_height = height - 8
135
+ visible = models[state.scroll_offset:state.scroll_offset + visible_height]
136
+ for i, (provider, model) in enumerate(visible):
137
+ row = 5 + i
138
+ idx = i + state.scroll_offset
139
+ if idx == state.selected_idx:
140
+ out.append(f"\033[{row};4H\033[7m> {model} ({provider})\033[0m")
141
+ else:
142
+ out.append(f"\033[{row};4H {model} \033[90m({provider})\033[0m")
143
+
144
+ out.append(f"\033[{height};1H\033[90m[j/k] Navigate [Enter] Select [q] Quit\033[0m")
145
+ sys.stdout.write(''.join(out))
146
+ sys.stdout.flush()
147
+
148
+ def handle_input(c):
149
+ if c == 'q':
150
+ return False
151
+ if c == '\x1b':
152
+ if select.select([sys.stdin], [], [], 0.05)[0]:
153
+ c2 = sys.stdin.read(1)
154
+ if c2 == '[':
155
+ c3 = sys.stdin.read(1)
156
+ if c3 == 'A' and state.phase == 'select_chat':
157
+ state.selected_idx = max(0, state.selected_idx - 1)
158
+ if state.selected_idx < state.scroll_offset:
159
+ state.scroll_offset = state.selected_idx
160
+ elif c3 == 'B' and state.phase == 'select_chat':
161
+ models = get_all_models()
162
+ _, height = get_size()
163
+ state.selected_idx = min(len(models) - 1, state.selected_idx + 1)
164
+ visible_height = height - 8
165
+ if state.selected_idx >= state.scroll_offset + visible_height:
166
+ state.scroll_offset = state.selected_idx - visible_height + 1
167
+ return True
168
+ if c == 'k' and state.phase == 'select_chat':
169
+ state.selected_idx = max(0, state.selected_idx - 1)
170
+ if state.selected_idx < state.scroll_offset:
171
+ state.scroll_offset = state.selected_idx
172
+ elif c == 'j' and state.phase == 'select_chat':
173
+ models = get_all_models()
174
+ _, height = get_size()
175
+ state.selected_idx = min(len(models) - 1, state.selected_idx + 1)
176
+ visible_height = height - 8
177
+ if state.selected_idx >= state.scroll_offset + visible_height:
178
+ state.scroll_offset = state.selected_idx - visible_height + 1
179
+ elif c == '\r' or c == '\n':
180
+ if state.phase == 'detect':
181
+ if get_all_models():
182
+ state.phase = 'select_chat'
183
+ state.selected_idx = 0
184
+ else:
185
+ return False
186
+ elif state.phase == 'select_chat':
187
+ models = get_all_models()
188
+ if models and state.selected_idx < len(models):
189
+ provider, model = models[state.selected_idx]
190
+ state.selections['chat_model'] = model
191
+ state.selections['chat_provider'] = provider
192
+ from npcsh.config import set_npcsh_config_value
193
+ set_npcsh_config_value('NPCSH_CHAT_MODEL', model)
194
+ set_npcsh_config_value('NPCSH_CHAT_PROVIDER', provider)
195
+ return False
196
+ return True
197
+
198
+ def run_detection():
199
+ state.status = "Detecting Ollama..."
200
+ render_screen()
201
+ ollama_models = detect_ollama_models()
202
+ state.providers_status['ollama'] = {'available': len(ollama_models) > 0, 'count': len(ollama_models)}
203
+ state.local_models.extend(ollama_models)
204
+
205
+ state.status = "Detecting LM Studio..."
206
+ render_screen()
207
+ lm_models = detect_lm_studio_models()
208
+ state.providers_status['lm_studio'] = {'available': len(lm_models) > 0, 'count': len(lm_models)}
209
+ state.local_models.extend(lm_models)
210
+
211
+ state.status = "Checking API keys..."
212
+ render_screen()
213
+ state.api_keys = detect_api_keys()
214
+ state.cloud_models = get_cloud_models(state.api_keys)
215
+
216
+ total = len(state.local_models) + len(state.cloud_models)
217
+ state.status = f"Found {total} models."
218
+
219
+ fd = sys.stdin.fileno()
220
+ old_settings = termios.tcgetattr(fd)
221
+
222
+ try:
223
+ tty.setcbreak(fd)
224
+ sys.stdout.write('\033[?25l')
225
+ run_detection()
226
+ render_screen()
227
+ while True:
228
+ c = sys.stdin.read(1)
229
+ if not handle_input(c):
230
+ break
231
+ render_screen()
232
+ finally:
233
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
234
+ sys.stdout.write('\033[?25h')
235
+ sys.stdout.write('\033[2J\033[H')
236
+ sys.stdout.flush()
237
+
238
+ if state.selections['chat_model']:
239
+ context['output'] = f"Setup complete! Model: {state.selections['chat_model']} ({state.selections['chat_provider']})"
240
+ else:
241
+ context['output'] = "Setup cancelled."
@@ -1,4 +1,4 @@
1
- jinx_name: studio.split_pane
1
+ jinx_name: studio_split_pane
2
2
  description: Split an existing pane to create a new pane alongside it.
3
3
  inputs:
4
4
  - paneId: "active"
@@ -118,7 +118,7 @@ steps:
118
118
  color = ''
119
119
 
120
120
  if idx == selected:
121
- sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
121
+ sys.stdout.write(f'\033[7;1m>{line.ljust(width-2)}\033[0m')
122
122
  elif color:
123
123
  sys.stdout.write(f'{color}{line}\033[0m')
124
124
  else:
@@ -1,4 +1,4 @@
1
- jinx_name: studio.switch_npc
1
+ jinx_name: studio_switch_npc
2
2
  description: Switch the active NPC in a chat pane.
3
3
  inputs:
4
4
  - paneId: "active"
@@ -1,4 +1,4 @@
1
- jinx_name: studio.switch_tab
1
+ jinx_name: studio_switch_tab
2
2
  description: Switch to a specific tab in a pane.
3
3
  inputs:
4
4
  - paneId: "active"
@@ -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()