npcsh 1.1.21__py3-none-any.whl → 1.1.23__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 (188) hide show
  1. npcsh/_state.py +282 -125
  2. npcsh/benchmark/npcsh_agent.py +77 -232
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
  4. npcsh/config.py +5 -2
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +8 -6
  7. npcsh/npc_team/corca.npc +5 -12
  8. npcsh/npc_team/frederic.npc +6 -9
  9. npcsh/npc_team/guac.npc +4 -4
  10. npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
  12. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  13. npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
  14. npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
  15. npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
  16. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
  17. npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
  19. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  20. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
  21. npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
  22. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  23. npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
  24. npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
  25. npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
  26. npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
  27. npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
  28. npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
  29. npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
  30. npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
  31. npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
  32. npcsh/npc_team/kadiefa.npc +6 -6
  33. npcsh/npc_team/npcsh.ctx +16 -0
  34. npcsh/npc_team/plonk.npc +5 -9
  35. npcsh/npc_team/sibiji.npc +15 -7
  36. npcsh/npcsh.py +1 -0
  37. npcsh/routes.py +0 -4
  38. npcsh/yap.py +22 -4
  39. npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
  40. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
  41. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
  42. npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
  43. npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
  44. npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
  45. npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
  46. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
  47. npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
  48. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
  49. npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  50. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
  51. npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
  52. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
  53. npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
  54. npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
  55. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
  56. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
  57. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
  58. npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
  59. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
  60. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
  61. npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
  62. npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
  63. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
  64. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
  65. npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
  66. npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
  67. npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
  68. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
  69. npcsh-1.1.23.dist-info/RECORD +216 -0
  70. npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
  71. npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
  72. npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
  73. npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
  74. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
  75. npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
  76. npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
  77. npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
  78. npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
  79. npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
  80. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
  81. npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
  82. npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
  83. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
  84. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
  85. npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
  86. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
  87. npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
  88. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
  89. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  90. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  91. npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
  92. npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
  93. npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
  94. npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
  95. npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
  96. npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
  97. npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
  98. npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  99. npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
  100. npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
  101. npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
  102. npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
  103. npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
  104. npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
  105. npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
  106. npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
  107. npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
  108. npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
  109. npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
  110. npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
  111. npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
  112. npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
  113. npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
  114. npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
  115. npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
  116. npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
  117. npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
  118. npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
  119. npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
  120. npcsh-1.1.21.dist-info/RECORD +0 -243
  121. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  122. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  123. /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
  124. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
  125. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  126. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  127. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  128. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  129. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
  130. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
  131. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  132. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  133. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
  134. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
  135. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
  136. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
  137. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  138. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  139. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
  140. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
  141. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
  142. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
  143. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  144. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  145. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  146. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  147. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  148. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
  149. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
  150. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  151. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
  152. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  153. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
  154. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
  155. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
  156. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
  157. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  158. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
  159. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
  160. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  161. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
  162. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  163. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
  164. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
  165. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
  166. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
  167. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  168. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
  169. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
  170. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
  171. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
  172. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
  173. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
  174. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
  175. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  181. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
  182. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
  183. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  184. {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
  185. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
  186. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
  187. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
  188. {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
@@ -1,41 +1,532 @@
1
- jinx_name: "init"
2
- description: "Initialize NPC project"
1
+ jinx_name: init
2
+ description: Interactive TUI for initializing NPC projects with model detection and configuration
3
3
  inputs:
4
- - directory: "."
5
- - templates: ""
6
- - context: ""
7
- - model: ""
8
- - provider: ""
4
+ - directory: ""
5
+ - templates: ""
6
+ - team_ctx: ""
7
+ - model: ""
8
+ - provider: ""
9
9
  steps:
10
- - name: "initialize_project"
11
- engine: "python"
10
+ - name: init_wizard
11
+ engine: python
12
12
  code: |
13
13
  import os
14
- import traceback
15
- from npcpy.npc_compiler import initialize_npc_project
16
-
17
- directory = context.get('directory')
18
- templates = context.get('templates')
19
- context_param = context.get('context') # Renamed to avoid conflict with Jinx context
20
- model = context.get('model')
21
- provider = context.get('provider')
22
- output_messages = context.get('messages', [])
23
-
24
- output_result = ""
25
- try:
26
- initialize_npc_project(
27
- directory=directory,
28
- templates=templates,
29
- context=context_param, # Use the renamed context parameter
30
- model=model,
31
- provider=provider
32
- )
33
- output_result = f"NPC project initialized in {os.path.abspath(directory)}."
34
- except NameError:
35
- output_result = "Init function (initialize_npc_project) not available."
36
- except Exception as e:
37
- traceback.print_exc()
38
- output_result = f"Error initializing project: {e}"
39
-
40
- context['output'] = output_result
41
- context['messages'] = output_messages
14
+ import sys
15
+ import tty
16
+ import termios
17
+ import select
18
+ from pathlib import Path
19
+
20
+ # Direct mode: if directory passed explicitly or non-interactive
21
+ _direct_dir = (context.get('directory') or '').strip()
22
+ _direct = bool(_direct_dir) or not sys.stdin.isatty()
23
+
24
+ if _direct:
25
+ from npcpy.npc_compiler import initialize_npc_project
26
+ try:
27
+ initialize_npc_project(
28
+ directory=_direct_dir or '.',
29
+ templates=context.get('templates'),
30
+ context=context.get('team_ctx'),
31
+ model=context.get('model'),
32
+ provider=context.get('provider')
33
+ )
34
+ context['output'] = f"NPC project initialized in {os.path.abspath(_direct_dir or '.')}"
35
+ except Exception as e:
36
+ context['output'] = f"Error: {e}"
37
+ context['messages'] = context.get('messages', [])
38
+
39
+ else:
40
+ # ========== Detection Functions ==========
41
+ def detect_ollama_models():
42
+ models = []
43
+ try:
44
+ import ollama
45
+ result = ollama.list()
46
+ for model in result.get('models', []):
47
+ name = model.get('model', model.get('name', ''))
48
+ if name:
49
+ models.append(name)
50
+ except:
51
+ pass
52
+ return models
53
+
54
+ def detect_lm_studio():
55
+ try:
56
+ import requests
57
+ resp = requests.get('http://localhost:1234/v1/models', timeout=2)
58
+ if resp.status_code == 200:
59
+ return [m.get('id', '') for m in resp.json().get('data', [])]
60
+ except:
61
+ pass
62
+ return []
63
+
64
+ def detect_api_keys():
65
+ keys = {}
66
+ for key in ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GEMINI_API_KEY', 'DEEPSEEK_API_KEY']:
67
+ if os.environ.get(key):
68
+ keys[key] = True
69
+ return keys
70
+
71
+ def get_cloud_models(api_keys):
72
+ models = []
73
+ if api_keys.get('ANTHROPIC_API_KEY'):
74
+ models.extend([('anthropic', 'claude-sonnet-4-20250514'), ('anthropic', 'claude-3-5-haiku-20241022')])
75
+ if api_keys.get('GEMINI_API_KEY'):
76
+ models.extend([('gemini', 'gemini-2.5-flash'), ('gemini', 'gemini-1.5-pro')])
77
+ if api_keys.get('DEEPSEEK_API_KEY'):
78
+ models.extend([('deepseek', 'deepseek-chat')])
79
+ return models
80
+
81
+ # ========== State ==========
82
+ class InitState:
83
+ def __init__(self):
84
+ self.phase = 0 # 0=dir, 1=detect, 2=model, 3=config, 4=confirm, 5=done
85
+ self.sel = 0
86
+ self.scroll = 0
87
+ self.directory = os.path.abspath(context.get('directory') or './npc_project')
88
+ self.editing = False
89
+ self.edit_buf = ""
90
+ self.edit_field = ""
91
+
92
+ # Detection results
93
+ self.ollama_models = []
94
+ self.lm_studio_models = []
95
+ self.cloud_models = []
96
+ self.api_keys = {}
97
+ self.all_models = [] # (provider, model) tuples
98
+
99
+ # Config
100
+ self.config = {
101
+ 'model': context.get('model') or '',
102
+ 'provider': context.get('provider') or '',
103
+ 'team_name': 'npc_team',
104
+ 'forenpc': 'forenpc',
105
+ 'context_desc': 'A team of AI agents',
106
+ }
107
+ self.config_keys = ['model', 'provider', 'team_name', 'forenpc', 'context_desc']
108
+ self.config_labels = {
109
+ 'model': 'Default Model',
110
+ 'provider': 'Default Provider',
111
+ 'team_name': 'Team Directory',
112
+ 'forenpc': 'Coordinator NPC',
113
+ 'context_desc': 'Team Description',
114
+ }
115
+ self.config_sel = 0
116
+
117
+ self.status = ""
118
+ self.error = ""
119
+ self.result = ""
120
+
121
+ ui = InitState()
122
+
123
+ # ========== Helpers ==========
124
+ def get_size():
125
+ try:
126
+ s = os.get_terminal_size()
127
+ return s.columns, s.lines
128
+ except:
129
+ return 80, 24
130
+
131
+ def run_detection():
132
+ ui.status = "Detecting Ollama models..."
133
+ render()
134
+ ui.ollama_models = detect_ollama_models()
135
+
136
+ ui.status = "Checking LM Studio..."
137
+ render()
138
+ ui.lm_studio_models = detect_lm_studio()
139
+
140
+ ui.status = "Checking API keys..."
141
+ render()
142
+ ui.api_keys = detect_api_keys()
143
+ ui.cloud_models = get_cloud_models(ui.api_keys)
144
+
145
+ # Build all_models list
146
+ ui.all_models = []
147
+ for m in ui.ollama_models:
148
+ ui.all_models.append(('ollama', m))
149
+ for m in ui.lm_studio_models:
150
+ ui.all_models.append(('lm_studio', m))
151
+ ui.all_models.extend(ui.cloud_models)
152
+
153
+ # Set defaults if found
154
+ if ui.all_models and not ui.config['model']:
155
+ ui.config['provider'], ui.config['model'] = ui.all_models[0]
156
+
157
+ total = len(ui.all_models)
158
+ ui.status = f"Found {total} models"
159
+
160
+ # ========== Rendering ==========
161
+ def render():
162
+ width, height = get_size()
163
+ out = []
164
+ out.append('\033[H') # Home cursor only, no full clear
165
+
166
+ phase_names = ['Directory', 'Detecting...', 'Select Model', 'Configure', 'Confirm', 'Complete']
167
+ phase_icons = ['1', '2', '3', '4', '5', '*']
168
+
169
+ # Progress bar
170
+ progress = ''
171
+ for i, name in enumerate(phase_names[:-1]):
172
+ if i < ui.phase:
173
+ progress += f'\033[32m[{phase_icons[i]}]\033[0m '
174
+ elif i == ui.phase:
175
+ progress += f'\033[33;1m[{phase_icons[i]}]\033[0m '
176
+ else:
177
+ progress += f'\033[90m[{phase_icons[i]}]\033[0m '
178
+
179
+ header = ' NPC INIT '
180
+ out.append(f'\033[1;1H\033[7;1m{header.ljust(width)}\033[0m')
181
+ out.append(f'\033[2;2H{progress}')
182
+
183
+ if ui.phase == 0:
184
+ render_directory(out, width, height)
185
+ elif ui.phase == 1:
186
+ render_detecting(out, width, height)
187
+ elif ui.phase == 2:
188
+ render_model_select(out, width, height)
189
+ elif ui.phase == 3:
190
+ render_config(out, width, height)
191
+ elif ui.phase == 4:
192
+ render_confirm(out, width, height)
193
+ elif ui.phase == 5:
194
+ render_done(out, width, height)
195
+
196
+ if ui.error:
197
+ out.append(f'\033[{height-1};1H\033[K\033[31m {ui.error[:width-3]}\033[0m')
198
+
199
+ sys.stdout.write(''.join(out))
200
+ sys.stdout.flush()
201
+
202
+ def render_directory(out, width, height):
203
+ banner = [
204
+ '\033[36m ██╗███╗ ██╗██╗████████╗\033[0m',
205
+ '\033[36m ██║████╗ ██║██║╚══██╔══╝\033[0m',
206
+ '\033[36m ██║██╔██╗ ██║██║ ██║ \033[0m',
207
+ '\033[36m ██║██║╚██╗██║██║ ██║ \033[0m',
208
+ '\033[36m ██║██║ ╚████║██║ ██║ \033[0m',
209
+ '\033[36m ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ \033[0m',
210
+ ]
211
+ for i, line in enumerate(banner):
212
+ out.append(f'\033[{4+i};3H{line}')
213
+
214
+ y = 4 + len(banner) + 2
215
+ out.append(f'\033[{y};3H\033[1mProject Directory:\033[0m')
216
+ y += 2
217
+
218
+ if ui.editing:
219
+ out.append(f'\033[{y};3H\033[7m > {ui.edit_buf}_ \033[0m')
220
+ else:
221
+ exists = os.path.exists(ui.directory)
222
+ status = '\033[33m(exists)\033[0m' if exists else '\033[32m(will create)\033[0m'
223
+ out.append(f'\033[{y};3H\033[7m > {ui.directory} \033[0m {status}')
224
+
225
+ y += 2
226
+ out.append(f'\033[{y};3H\033[90mThis will create:\033[0m')
227
+ y += 1
228
+ preview_dir = ui.edit_buf if ui.editing else ui.directory
229
+ out.append(f'\033[{y};5H\033[90m{preview_dir}/\033[0m')
230
+ out.append(f'\033[{y+1};5H\033[90m├── npc_team/\033[0m')
231
+ out.append(f'\033[{y+2};5H\033[90m│ ├── forenpc.npc\033[0m')
232
+ out.append(f'\033[{y+3};5H\033[90m│ ├── team.ctx\033[0m')
233
+ out.append(f'\033[{y+4};5H\033[90m│ ├── jinxs/\033[0m')
234
+ out.append(f'\033[{y+5};5H\033[90m│ └── tools/\033[0m')
235
+ out.append(f'\033[{y+6};5H\033[90m├── images/\033[0m')
236
+ out.append(f'\033[{y+7};5H\033[90m├── models/\033[0m')
237
+ out.append(f'\033[{y+8};5H\033[90m└── mcp_servers/\033[0m')
238
+
239
+ if ui.editing:
240
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:Confirm Esc:Cancel \033[0m'.ljust(width))
241
+ else:
242
+ out.append(f'\033[{height};1H\033[K\033[7m e:Edit Enter:Next q:Quit \033[0m'.ljust(width))
243
+
244
+ def render_detecting(out, width, height):
245
+ mid = height // 2
246
+ out.append(f'\033[{mid-1};3H\033[1mDetecting available models...\033[0m')
247
+ out.append(f'\033[{mid+1};3H\033[33m{ui.status}\033[0m')
248
+
249
+ y = mid + 3
250
+ # Show what we've found so far
251
+ if ui.ollama_models:
252
+ out.append(f'\033[{y};5H\033[32m✓\033[0m Ollama: {len(ui.ollama_models)} models')
253
+ y += 1
254
+ if ui.lm_studio_models:
255
+ out.append(f'\033[{y};5H\033[32m✓\033[0m LM Studio: {len(ui.lm_studio_models)} models')
256
+ y += 1
257
+ for key in ['ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'DEEPSEEK_API_KEY']:
258
+ if ui.api_keys.get(key):
259
+ short = key.replace('_API_KEY', '').lower()
260
+ out.append(f'\033[{y};5H\033[32m✓\033[0m {short}')
261
+ y += 1
262
+
263
+ def render_model_select(out, width, height):
264
+ y = 4
265
+ out.append(f'\033[{y};3H\033[1mSelect Default Model:\033[0m')
266
+ y += 1
267
+
268
+ # Show detection summary
269
+ out.append(f'\033[{y};3H\033[90mDetected: {len(ui.ollama_models)} ollama, {len(ui.lm_studio_models)} lm_studio, {len(ui.cloud_models)} cloud\033[0m')
270
+ y += 2
271
+
272
+ list_height = height - 10
273
+ visible = ui.all_models[ui.scroll:ui.scroll + list_height]
274
+
275
+ for i, (provider, model) in enumerate(visible):
276
+ idx = ui.scroll + i
277
+ row = y + i
278
+
279
+ # Provider color
280
+ pcolor = {'ollama': '\033[33m', 'lm_studio': '\033[35m', 'anthropic': '\033[34m',
281
+ 'gemini': '\033[32m', 'deepseek': '\033[36m'}.get(provider, '\033[37m')
282
+
283
+ if idx == ui.sel:
284
+ out.append(f'\033[{row};2H\033[7;1m > {model:<40} {pcolor}{provider}\033[0m\033[7m \033[0m')
285
+ else:
286
+ out.append(f'\033[{row};2H {model:<40} {pcolor}{provider}\033[0m')
287
+
288
+ if not ui.all_models:
289
+ out.append(f'\033[{y};3H\033[31mNo models found! Install Ollama or set API keys.\033[0m')
290
+ out.append(f'\033[{y+2};3H\033[90mYou can still continue and configure manually.\033[0m')
291
+
292
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Navigate Enter:Select s:Skip q:Quit \033[0m'.ljust(width))
293
+
294
+ def render_config(out, width, height):
295
+ y = 4
296
+ out.append(f'\033[{y};3H\033[1mConfigure Team:\033[0m')
297
+ y += 2
298
+
299
+ for i, key in enumerate(ui.config_keys):
300
+ label = ui.config_labels[key]
301
+ val = ui.config[key]
302
+ row = y + i * 2
303
+
304
+ if ui.editing and ui.edit_field == key:
305
+ out.append(f'\033[{row};3H\033[33m{label}:\033[0m')
306
+ out.append(f'\033[{row+1};5H\033[7m {ui.edit_buf}_ \033[0m')
307
+ elif i == ui.config_sel:
308
+ out.append(f'\033[{row};3H\033[1m{label}:\033[0m')
309
+ out.append(f'\033[{row+1};5H\033[7m {val or "(not set)"} \033[0m')
310
+ else:
311
+ out.append(f'\033[{row};3H\033[90m{label}:\033[0m')
312
+ not_set = '\033[90m(not set)\033[0m'
313
+ out.append(f'\033[{row+1};5H {val or not_set}')
314
+
315
+ if ui.editing:
316
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:Save Esc:Cancel \033[0m'.ljust(width))
317
+ else:
318
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Navigate e:Edit Enter:Next Backspace:Back q:Quit \033[0m'.ljust(width))
319
+
320
+ def render_confirm(out, width, height):
321
+ y = 4
322
+ out.append(f'\033[{y};3H\033[1mReady to Initialize:\033[0m')
323
+ y += 2
324
+
325
+ out.append(f'\033[{y};5H\033[1mDirectory:\033[0m {ui.directory}')
326
+ y += 1
327
+ out.append(f'\033[{y};5H\033[1mModel:\033[0m {ui.config["model"]} ({ui.config["provider"]})')
328
+ y += 1
329
+ out.append(f'\033[{y};5H\033[1mTeam:\033[0m {ui.config["team_name"]}')
330
+ y += 1
331
+ out.append(f'\033[{y};5H\033[1mCoordinator:\033[0m {ui.config["forenpc"]}')
332
+ y += 1
333
+ out.append(f'\033[{y};5H\033[1mDescription:\033[0m {ui.config["context_desc"]}')
334
+ y += 2
335
+
336
+ out.append(f'\033[{y};3H\033[90mWill create:\033[0m')
337
+ y += 1
338
+ files = [
339
+ f'{ui.directory}/{ui.config["team_name"]}/team.ctx',
340
+ f'{ui.directory}/{ui.config["team_name"]}/{ui.config["forenpc"]}.npc',
341
+ f'{ui.directory}/{ui.config["team_name"]}/jinxs/',
342
+ f'{ui.directory}/{ui.config["team_name"]}/tools/',
343
+ f'{ui.directory}/images/',
344
+ f'{ui.directory}/models/',
345
+ f'{ui.directory}/mcp_servers/',
346
+ ]
347
+ for f in files:
348
+ out.append(f'\033[{y};5H\033[90m{f}\033[0m')
349
+ y += 1
350
+
351
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:Create Backspace:Back q:Quit \033[0m'.ljust(width))
352
+
353
+ def render_done(out, width, height):
354
+ y = 4
355
+ if ui.error:
356
+ out.append(f'\033[{y};3H\033[31;1mInitialization Failed\033[0m')
357
+ y += 2
358
+ out.append(f'\033[{y};3H\033[31m{ui.error}\033[0m')
359
+ else:
360
+ out.append(f'\033[{y};3H\033[32;1mProject Initialized!\033[0m')
361
+ y += 2
362
+ out.append(f'\033[{y};3H{ui.result}')
363
+ y += 2
364
+ out.append(f'\033[{y};3H\033[1mNext steps:\033[0m')
365
+ y += 1
366
+ out.append(f'\033[{y};5H\033[36mcd {ui.directory}\033[0m')
367
+ y += 1
368
+ out.append(f'\033[{y};5H\033[36mnpcsh\033[0m')
369
+ y += 2
370
+ out.append(f'\033[{y};3H\033[90mOr add more NPCs:\033[0m')
371
+ y += 1
372
+ out.append(f'\033[{y};5H\033[90mCreate {ui.config["team_name"]}/analyst.npc, {ui.config["team_name"]}/writer.npc, etc.\033[0m')
373
+
374
+ out.append(f'\033[{height};1H\033[K\033[7m Enter/q:Exit \033[0m'.ljust(width))
375
+
376
+ # ========== Execution ==========
377
+ def do_init():
378
+ from npcpy.npc_compiler import initialize_npc_project
379
+ try:
380
+ initialize_npc_project(
381
+ directory=ui.directory,
382
+ templates=context.get('templates'),
383
+ context=ui.config['context_desc'],
384
+ model=ui.config['model'],
385
+ provider=ui.config['provider']
386
+ )
387
+ ui.result = f"Created project at {os.path.abspath(ui.directory)}"
388
+ ui.error = ""
389
+ except Exception as e:
390
+ ui.error = str(e)
391
+ ui.result = ""
392
+
393
+ # ========== Input ==========
394
+ def handle_input(c, fd):
395
+ if ui.editing:
396
+ return handle_edit(c, fd)
397
+
398
+ # Escape sequences
399
+ if c == '\x1b':
400
+ if select.select([fd], [], [], 0.05)[0]:
401
+ c2 = os.read(fd, 1).decode('latin-1')
402
+ if c2 == '[':
403
+ c3 = os.read(fd, 1).decode('latin-1')
404
+ if c3 == 'A': move_up()
405
+ elif c3 == 'B': move_down()
406
+ return True
407
+
408
+ if c == 'q' or c == '\x03':
409
+ return False
410
+
411
+ if ui.phase == 0: # Directory
412
+ if c == 'e':
413
+ ui.editing = True
414
+ ui.edit_buf = ui.directory
415
+ elif c in ('\r', '\n'):
416
+ ui.phase = 1
417
+ render()
418
+ run_detection()
419
+ ui.phase = 2
420
+ ui.sel = 0
421
+ ui.scroll = 0
422
+
423
+ elif ui.phase == 2: # Model select
424
+ if c == 'j': move_down()
425
+ elif c == 'k': move_up()
426
+ elif c == 's': # Skip
427
+ ui.phase = 3
428
+ ui.config_sel = 0
429
+ elif c in ('\r', '\n'):
430
+ if ui.all_models and ui.sel < len(ui.all_models):
431
+ ui.config['provider'], ui.config['model'] = ui.all_models[ui.sel]
432
+ ui.phase = 3
433
+ ui.config_sel = 0
434
+
435
+ elif ui.phase == 3: # Config
436
+ if c == 'j': move_down()
437
+ elif c == 'k': move_up()
438
+ elif c == 'e':
439
+ key = ui.config_keys[ui.config_sel]
440
+ ui.editing = True
441
+ ui.edit_field = key
442
+ ui.edit_buf = ui.config[key]
443
+ elif c == '\x7f' or c == '\x08': # Backspace
444
+ ui.phase = 2
445
+ ui.sel = 0
446
+ elif c in ('\r', '\n'):
447
+ ui.phase = 4
448
+
449
+ elif ui.phase == 4: # Confirm
450
+ if c == '\x7f' or c == '\x08':
451
+ ui.phase = 3
452
+ ui.config_sel = 0
453
+ elif c in ('\r', '\n'):
454
+ ui.phase = 5
455
+ render()
456
+ do_init()
457
+
458
+ elif ui.phase == 5: # Done
459
+ if c in ('\r', '\n', 'q'):
460
+ return False
461
+
462
+ return True
463
+
464
+ def handle_edit(c, fd):
465
+ if c == '\x1b':
466
+ if select.select([fd], [], [], 0.05)[0]:
467
+ os.read(fd, 2)
468
+ ui.editing = False
469
+ ui.edit_buf = ""
470
+ return True
471
+ if c in ('\r', '\n'):
472
+ if ui.phase == 0:
473
+ ui.directory = os.path.abspath(os.path.expanduser(ui.edit_buf)) if ui.edit_buf else ui.directory
474
+ elif ui.phase == 3:
475
+ ui.config[ui.edit_field] = ui.edit_buf
476
+ ui.editing = False
477
+ return True
478
+ if c == '\x7f' or c == '\x08':
479
+ ui.edit_buf = ui.edit_buf[:-1]
480
+ return True
481
+ if c >= ' ' and c <= '~':
482
+ ui.edit_buf += c
483
+ return True
484
+
485
+ def move_up():
486
+ if ui.phase == 2:
487
+ ui.sel = max(0, ui.sel - 1)
488
+ if ui.sel < ui.scroll:
489
+ ui.scroll = ui.sel
490
+ elif ui.phase == 3:
491
+ ui.config_sel = max(0, ui.config_sel - 1)
492
+
493
+ def move_down():
494
+ _, height = get_size()
495
+ if ui.phase == 2:
496
+ ui.sel = min(len(ui.all_models) - 1, ui.sel + 1) if ui.all_models else 0
497
+ list_height = height - 10
498
+ if ui.sel >= ui.scroll + list_height:
499
+ ui.scroll = ui.sel - list_height + 1
500
+ elif ui.phase == 3:
501
+ ui.config_sel = min(len(ui.config_keys) - 1, ui.config_sel + 1)
502
+
503
+ # ========== Main Loop ==========
504
+ fd = sys.stdin.fileno()
505
+ old_settings = termios.tcgetattr(fd)
506
+
507
+ try:
508
+ tty.setcbreak(fd)
509
+ sys.stdout.write('\033[?25l') # hide cursor
510
+ sys.stdout.write('\033[2J\033[H') # initial full clear
511
+ sys.stdout.flush()
512
+ render()
513
+
514
+ while True:
515
+ c = os.read(fd, 1).decode('latin-1')
516
+ if not handle_input(c, fd):
517
+ break
518
+ render()
519
+ finally:
520
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
521
+ sys.stdout.write('\033[?25h\033[2J\033[H')
522
+ sys.stdout.flush()
523
+
524
+ if ui.result:
525
+ print(ui.result)
526
+ context['output'] = ui.result
527
+ elif ui.error:
528
+ context['output'] = f"Error: {ui.error}"
529
+ else:
530
+ context['output'] = "Init cancelled."
531
+
532
+ context['messages'] = context.get('messages', [])
@@ -1,6 +1,5 @@
1
1
  jinx_name: jinxs
2
2
  description: Interactive jinx browser - browse, search, and preview available jinxs
3
- interactive: true
4
3
  inputs: []
5
4
  steps:
6
5
  - name: jinxs_browser