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
@@ -24,6 +24,25 @@ steps:
24
24
 
25
25
  from npcpy.llm_funcs import get_llm_response
26
26
 
27
+ # Helper to log jinx executions to DB
28
+ def _log_jinx(trigger_id, npc_name, inputs, output, status="success", error_msg=None):
29
+ try:
30
+ if state and hasattr(state, 'command_history') and state.command_history is not None and hasattr(state.command_history, 'save_jinx_execution'):
31
+ _conv_id = getattr(state, 'conversation_id', None) or ''
32
+ state.command_history.save_jinx_execution(
33
+ triggering_message_id=f"{_conv_id}-{trigger_id}",
34
+ conversation_id=_conv_id,
35
+ npc_name=npc_name,
36
+ jinx_name="plonk",
37
+ jinx_inputs=inputs,
38
+ jinx_output=str(output) if output else "",
39
+ status=status,
40
+ team_name=state.team.name if state and hasattr(state, 'team') and state.team else None,
41
+ error_message=error_msg,
42
+ )
43
+ except Exception:
44
+ pass
45
+
27
46
  try:
28
47
  from npcpy.data.image import capture_screenshot
29
48
  from npcpy.work.desktop import perform_action
@@ -123,7 +142,7 @@ steps:
123
142
  render_screen()
124
143
 
125
144
  try:
126
- ss = capture_screenshot()
145
+ ss = capture_screenshot(full=True)
127
146
  if not ss or 'file_path' not in ss:
128
147
  task['actions'].append({"action": "fail", "reason": "Screenshot failed"})
129
148
  task['status'] = 'failed'
@@ -136,23 +155,43 @@ steps:
136
155
  history_context = ""
137
156
  if task['actions']:
138
157
  history_context = "\nPrevious actions:\n"
139
- for i, act in enumerate(task['actions'][-5:], 1):
158
+ for i, act in enumerate(task['actions'][-8:], 1):
140
159
  history_context += " " + str(i) + ". " + act.get('action', '?')
141
160
  if act.get('x'):
142
161
  history_context += " at (" + str(act.get('x', '?')) + ", " + str(act.get('y', '?')) + ")"
162
+ if act.get('text'):
163
+ history_context += ' "' + act.get('text', '')[:50] + '"'
164
+ if act.get('command'):
165
+ history_context += ' [' + act.get('command', '')[:30] + ']'
143
166
  history_context += " - " + act.get('reason', '') + "\n"
167
+ # Detect repetition: if last 3 actions are the same type, warn
168
+ recent = task['actions'][-3:]
169
+ if len(recent) == 3 and all(a.get('action') == recent[0].get('action') for a in recent):
170
+ history_context += "\n*** WARNING: You have repeated '" + recent[0].get('action', '?') + "' 3 times. "
171
+ history_context += "This is NOT working. You MUST try a completely different approach. ***\n"
144
172
 
145
173
  prompt = "You are a GUI automation assistant. Analyze this screenshot and determine the next action.\n\n"
174
+ prompt += "CRITICAL RULES:\n"
175
+ prompt += "1. VERIFY: Check the screenshot BEFORE acting. Does it show the result of your last action? If not, wait.\n"
176
+ prompt += "2. NO REPEATS: If an action failed or had no effect, try a DIFFERENT approach. Never repeat the same action.\n"
177
+ prompt += "3. FULL TEXT: Always type the COMPLETE text in a single 'type' action. Never truncate or split text.\n"
178
+ prompt += "4. FOCUS FIRST: Before typing, make sure the target field is focused (click it or use keyboard shortcut).\n\n"
179
+ prompt += "KEYBOARD SHORTCUTS (use 'key' action with these):\n"
180
+ prompt += "- ctrl+l or F6: Focus browser address/search bar\n"
181
+ prompt += "- ctrl+a: Select all text in current field\n"
182
+ prompt += "- ctrl+t: New browser tab\n"
183
+ prompt += "- tab/shift+tab: Move between form fields\n"
184
+ prompt += "- escape: Close popups, cancel autocomplete\n\n"
146
185
  prompt += "TASK: " + task['text'] + "\n"
147
186
  prompt += history_context + "\n"
148
187
  prompt += "Available actions:\n"
149
- prompt += "- click: Click at x,y coordinates (0-100 percentage of screen)\n"
150
- prompt += "- type: Type text (use 'text' field)\n"
151
- prompt += "- key: Press key like enter, tab, escape (use 'text' field)\n"
152
- prompt += "- launch: Launch application (use 'command' field, e.g. " + app_examples + ")\n"
153
- prompt += "- wait: Wait for 'duration' seconds\n"
154
- prompt += "- done: Task completed successfully\n"
155
- prompt += "- fail: Task cannot be completed\n\n"
188
+ prompt += "- click: Click at x,y (0-100 % of screen). Fields: x, y\n"
189
+ prompt += "- type: Type text into focused field. Fields: text (MUST be complete text)\n"
190
+ prompt += "- key: Press key(s). Fields: text (e.g. 'enter', 'ctrl+l', 'tab', 'escape')\n"
191
+ prompt += "- launch: Launch application. Fields: command (e.g. " + app_examples + ")\n"
192
+ prompt += "- wait: Wait seconds. Fields: duration\n"
193
+ prompt += "- done: Task completed\n"
194
+ prompt += "- fail: Task impossible\n\n"
156
195
  prompt += "Respond with JSON, e.g.: " + json_schema_example
157
196
 
158
197
  ui.status = "Thinking... (iter " + str(ui.iteration) + "/" + str(ui.max_iter) + ")"
@@ -173,21 +212,38 @@ steps:
173
212
  action_response = json.loads(action_response)
174
213
  except:
175
214
  task['actions'].append({"action": "error", "reason": "Invalid JSON from model"})
215
+ _log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}",
216
+ npc.name if npc and hasattr(npc, 'name') else "plonk",
217
+ {"task": task['text'], "iteration": ui.iteration, "type": "analysis"},
218
+ str(resp.get('response', '')), status="error", error_msg="Invalid JSON from model")
176
219
  ui.status = "Bad response, retrying..."
177
220
  return
178
221
 
179
222
  action = action_response.get('action', 'fail')
180
223
  reason = action_response.get('reason', '')
181
224
 
225
+ _npc_name = npc.name if npc and hasattr(npc, 'name') else "plonk"
226
+ _log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}",
227
+ _npc_name,
228
+ {"task": task['text'], "iteration": ui.iteration, "screenshot": screenshot_path,
229
+ "type": "analysis"},
230
+ json.dumps(action_response))
231
+
182
232
  if action == 'done':
183
233
  task['status'] = 'done'
184
234
  task['actions'].append({"action": "done", "reason": reason})
235
+ _log_jinx(f"plonk-task-{ui.current_task}-done", _npc_name,
236
+ {"task": task['text'], "type": "task_complete", "total_actions": len(task['actions'])},
237
+ reason)
185
238
  advance_to_next_task()
186
239
  return
187
240
 
188
241
  if action == 'fail':
189
242
  task['status'] = 'failed'
190
243
  task['actions'].append({"action": "fail", "reason": reason})
244
+ _log_jinx(f"plonk-task-{ui.current_task}-fail", _npc_name,
245
+ {"task": task['text'], "type": "task_failed", "total_actions": len(task['actions'])},
246
+ reason, status="error", error_msg=reason)
191
247
  advance_to_next_task()
192
248
  return
193
249
 
@@ -196,20 +252,25 @@ steps:
196
252
 
197
253
  if action == 'click':
198
254
  x, y = action_response.get('x', 50), action_response.get('y', 50)
199
- perform_action('click', x=x, y=y)
255
+ perform_action({"type": "click", "x": x, "y": y})
200
256
  act_record['x'] = x
201
257
  act_record['y'] = y
202
258
  elif action == 'type':
203
259
  txt = action_response.get('text', '')
204
- perform_action('type', text=txt)
260
+ perform_action({"type": "type", "text": txt})
205
261
  act_record['text'] = txt
206
262
  elif action == 'key':
207
263
  key = action_response.get('text', 'enter')
208
- perform_action('key', key=key)
264
+ # Detect combo keys like ctrl+l, ctrl+a, shift+tab
265
+ if '+' in key:
266
+ parts = [k.strip() for k in key.split('+')]
267
+ perform_action({"type": "hotkey", "keys": parts})
268
+ else:
269
+ perform_action({"type": "key", "keys": key})
209
270
  act_record['key'] = key
210
271
  elif action == 'launch':
211
272
  cmd = action_response.get('command', '')
212
- perform_action('launch', command=cmd)
273
+ perform_action({"type": "shell", "command": cmd})
213
274
  act_record['command'] = cmd
214
275
  time.sleep(2)
215
276
  elif action == 'wait':
@@ -219,7 +280,13 @@ steps:
219
280
 
220
281
  task['actions'].append(act_record)
221
282
  ui.status = action + " - " + reason[:40]
222
- time.sleep(0.3)
283
+ # Wait for UI to settle after state-changing actions
284
+ if action in ('key', 'click'):
285
+ time.sleep(2.0)
286
+ elif action == 'type':
287
+ time.sleep(0.5)
288
+ else:
289
+ time.sleep(0.3)
223
290
 
224
291
  if ui.mode == 'step':
225
292
  ui.mode = 'paused'
@@ -227,6 +294,10 @@ steps:
227
294
 
228
295
  except Exception as e:
229
296
  task['actions'].append({"action": "error", "reason": str(e)})
297
+ _log_jinx(f"plonk-task-{ui.current_task}-iter-{ui.iteration}-error",
298
+ npc.name if npc and hasattr(npc, 'name') else "plonk",
299
+ {"task": task['text'] if task else "unknown", "iteration": ui.iteration, "type": "error"},
300
+ str(e), status="error", error_msg=str(e))
230
301
  ui.status = "Error: " + str(e)[:40]
231
302
 
232
303
  def advance_to_next_task():
@@ -363,7 +434,7 @@ steps:
363
434
  if a.get('x') is not None:
364
435
  coords = "(" + str(a.get('x', '')) + "," + str(a.get('y', '')) + ") "
365
436
  elif a.get('text'):
366
- coords = '"' + str(a['text'])[:15] + '" '
437
+ coords = '"' + str(a['text'])[:40] + '" '
367
438
  elif a.get('key'):
368
439
  coords = '[' + str(a['key']) + '] '
369
440
  elif a.get('command'):
@@ -11,15 +11,11 @@ colors:
11
11
  top: "34,139,34"
12
12
  bottom: "139,69,19"
13
13
  primary_directive: |
14
- You are plonk, the browser and GUI automation specialist.
15
-
16
- Browser tools: open_browser, browser_action, browser_screenshot, close_browser
17
-
18
- browser_action actions: click, type, type_and_enter, select, wait, scroll, get_text, get_page, get_elements, press_key
19
-
20
- Use get_elements to discover selectors on the page. Use xpath:// prefix for XPath selectors.
21
-
22
- Desktop tools: screenshot, click, type_text, key_press, launch_app, wait
14
+ You are plonk, the automation specialist for browser and desktop.
15
+ Browser: open_browser, browser_action, browser_screenshot, close_browser.
16
+ browser_action: click, type, type_and_enter, select, wait, scroll, get_text, get_page, get_elements, press_key.
17
+ Desktop: screenshot, click, type_text, key_press, launch_app, wait.
18
+ Use get_elements to find selectors. Use xpath:// for XPath. Screenshot after each step to verify.
23
19
  jinxs:
24
20
  - lib/browser/*
25
21
  - lib/computer_use/*
@@ -0,0 +1,378 @@
1
+ jinx_name: "roll"
2
+ description: "Video creation studio TUI - generate and manage videos with parameter controls"
3
+ interactive: true
4
+ inputs:
5
+ - prompt: null
6
+ - model: null
7
+ - provider: null
8
+ - output_path: null
9
+ - num_frames: null
10
+ - width: null
11
+ - height: null
12
+ steps:
13
+ - name: "roll_tui"
14
+ engine: "python"
15
+ code: |
16
+ import os
17
+ import sys
18
+ import tty
19
+ import termios
20
+ import select
21
+ from datetime import datetime
22
+
23
+ from npcpy.llm_funcs import gen_video
24
+
25
+ npc = context.get('npc')
26
+ if isinstance(npc, str):
27
+ npc = None
28
+
29
+ # ========== State ==========
30
+ class RollState:
31
+ def __init__(self):
32
+ self.prompt = ""
33
+ self.model = ""
34
+ self.provider = ""
35
+ self.width_val = 256
36
+ self.height_val = 256
37
+ self.num_frames = 125
38
+ self.output_path = ""
39
+ # UI state
40
+ self.params = ['prompt', 'model', 'provider', 'width', 'height', 'num_frames', 'output']
41
+ self.sel = 0
42
+ self.scroll = 0
43
+ self.mode = 'params' # params, editing, gallery, generating
44
+ self.edit_buf = ""
45
+ self.edit_cursor = 0
46
+ self.status = "Ready"
47
+ self.gallery = [] # [{"path": str, "prompt": str, "timestamp": str}]
48
+ self.gallery_sel = 0
49
+ self.gallery_scroll = 0
50
+
51
+ ui = RollState()
52
+
53
+ # Load from context
54
+ ui.prompt = str(context.get('prompt') or '')
55
+ ui.model = str(context.get('model') or os.getenv('NPCSH_VIDEO_GEN_MODEL', ''))
56
+ ui.provider = str(context.get('provider') or os.getenv('NPCSH_VIDEO_GEN_PROVIDER', ''))
57
+ if not ui.model and npc and hasattr(npc, 'model') and npc.model:
58
+ ui.model = npc.model
59
+ if not ui.provider and npc and hasattr(npc, 'provider') and npc.provider:
60
+ ui.provider = npc.provider
61
+ if not ui.model:
62
+ ui.model = "stable-video-diffusion"
63
+ if not ui.provider:
64
+ ui.provider = "diffusers"
65
+ try:
66
+ ui.width_val = int(context.get('width') or 256)
67
+ except:
68
+ pass
69
+ try:
70
+ ui.height_val = int(context.get('height') or 256)
71
+ except:
72
+ pass
73
+ try:
74
+ ui.num_frames = int(context.get('num_frames') or 125)
75
+ except:
76
+ pass
77
+ ui.output_path = str(context.get('output_path') or '')
78
+
79
+ # Load existing gallery
80
+ vid_dir = os.path.expanduser("~/.npcsh/videos/")
81
+ if os.path.isdir(vid_dir):
82
+ for f in sorted(os.listdir(vid_dir), reverse=True)[:50]:
83
+ if f.lower().endswith(('.mp4', '.webm', '.avi', '.mov', '.mkv')):
84
+ ui.gallery.append({"path": os.path.join(vid_dir, f), "prompt": "", "timestamp": f})
85
+
86
+ def get_size():
87
+ try:
88
+ s = os.get_terminal_size()
89
+ return s.columns, s.lines
90
+ except:
91
+ return 80, 24
92
+
93
+ def get_param_value(idx):
94
+ p = ui.params[idx]
95
+ if p == 'prompt': return ui.prompt
96
+ if p == 'model': return ui.model or '(default)'
97
+ if p == 'provider': return ui.provider or '(default)'
98
+ if p == 'width': return str(ui.width_val)
99
+ if p == 'height': return str(ui.height_val)
100
+ if p == 'num_frames': return str(ui.num_frames)
101
+ if p == 'output': return ui.output_path or '(auto)'
102
+ return ''
103
+
104
+ def set_param_value(idx, val):
105
+ p = ui.params[idx]
106
+ if p == 'prompt': ui.prompt = val
107
+ elif p == 'model': ui.model = val
108
+ elif p == 'provider': ui.provider = val
109
+ elif p == 'width':
110
+ try: ui.width_val = int(val)
111
+ except: pass
112
+ elif p == 'height':
113
+ try: ui.height_val = int(val)
114
+ except: pass
115
+ elif p == 'num_frames':
116
+ try: ui.num_frames = max(1, int(val))
117
+ except: pass
118
+ elif p == 'output': ui.output_path = val
119
+
120
+ def generate_videos():
121
+ ui.mode = 'generating'
122
+ ui.status = "Generating..."
123
+ render_screen()
124
+
125
+ if not ui.prompt:
126
+ ui.status = "Error: No prompt provided"
127
+ ui.mode = 'params'
128
+ return
129
+
130
+ try:
131
+ if ui.output_path and ui.output_path.strip():
132
+ out_file = os.path.expanduser(ui.output_path)
133
+ else:
134
+ os.makedirs(vid_dir, exist_ok=True)
135
+ out_file = os.path.join(vid_dir, "roll_" + datetime.now().strftime('%Y%m%d_%H%M%S') + ".mp4")
136
+
137
+ result = gen_video(
138
+ prompt=ui.prompt,
139
+ model=ui.model or None,
140
+ provider=ui.provider or None,
141
+ npc=npc,
142
+ num_frames=ui.num_frames,
143
+ width=ui.width_val,
144
+ height=ui.height_val,
145
+ output_path=out_file,
146
+ **context.get('api_kwargs', {})
147
+ )
148
+
149
+ if isinstance(result, dict):
150
+ msg = result.get('output', 'Video generated.')
151
+ else:
152
+ msg = str(result)
153
+
154
+ ui.gallery.insert(0, {"path": out_file, "prompt": ui.prompt, "timestamp": os.path.basename(out_file)})
155
+ ui.status = "Generated: " + os.path.basename(out_file)
156
+ except Exception as e:
157
+ ui.status = "Error: " + str(e)[:60]
158
+
159
+ ui.mode = 'params'
160
+
161
+ # ========== Rendering ==========
162
+ def render_screen():
163
+ width, height = get_size()
164
+ out = []
165
+ out.append("\033[H")
166
+
167
+ header = " ROLL - Video Creation Studio "
168
+ out.append("\033[1;1H\033[7;1m" + header.ljust(width) + "\033[0m")
169
+
170
+ if ui.mode in ('params', 'editing', 'generating'):
171
+ # Parameter panel
172
+ out.append("\033[3;1H\033[36;1m Parameters \033[90m" + ("-" * (width - 13)) + "\033[0m")
173
+
174
+ for i, p in enumerate(ui.params):
175
+ row = 4 + i
176
+ out.append("\033[" + str(row) + ";1H\033[K")
177
+ label = p.capitalize() + ":"
178
+ val = get_param_value(i)
179
+
180
+ if ui.mode == 'editing' and i == ui.sel:
181
+ line = " " + label.ljust(14) + "\033[7m " + ui.edit_buf + " \033[0m"
182
+ else:
183
+ line = " " + label.ljust(14) + val[:width - 18]
184
+
185
+ if i == ui.sel and ui.mode != 'editing':
186
+ out.append("\033[7m>" + line + "\033[0m")
187
+ else:
188
+ out.append(" " + line)
189
+
190
+ # Gallery preview
191
+ gallery_row = 4 + len(ui.params) + 1
192
+ out.append("\033[" + str(gallery_row) + ";1H\033[33;1m Gallery (" + str(len(ui.gallery)) + ") \033[90m" + ("-" * (width - 20)) + "\033[0m")
193
+
194
+ gallery_h = height - gallery_row - 4
195
+ for i in range(max(0, gallery_h)):
196
+ idx = ui.gallery_scroll + i
197
+ row = gallery_row + 1 + i
198
+ out.append("\033[" + str(row) + ";1H\033[K")
199
+ if idx >= len(ui.gallery):
200
+ continue
201
+ g = ui.gallery[idx]
202
+ fname = os.path.basename(g['path'])[:width - 6]
203
+ out.append(" " + fname)
204
+
205
+ elif ui.mode == 'gallery':
206
+ out.append("\033[3;1H\033[33;1m Gallery (" + str(len(ui.gallery)) + ") \033[90m" + ("-" * (width - 20)) + "\033[0m")
207
+
208
+ gallery_h = height - 6
209
+ for i in range(gallery_h):
210
+ idx = ui.gallery_scroll + i
211
+ row = 4 + i
212
+ out.append("\033[" + str(row) + ";1H\033[K")
213
+ if idx >= len(ui.gallery):
214
+ continue
215
+ g = ui.gallery[idx]
216
+ fname = os.path.basename(g['path'])
217
+ if idx == ui.gallery_sel:
218
+ out.append("\033[7m> " + fname[:width-4] + "\033[0m")
219
+ else:
220
+ out.append(" " + fname[:width-4])
221
+
222
+ # Status + footer
223
+ out.append("\033[" + str(height-2) + ";1H\033[K\033[90m" + ("-" * width) + "\033[0m")
224
+ out.append("\033[" + str(height-1) + ";1H\033[K " + ui.status[:width-2])
225
+
226
+ if ui.mode == 'editing':
227
+ footer = " Type value, Enter:Confirm Esc:Cancel "
228
+ elif ui.mode == 'gallery':
229
+ footer = " j/k:Nav o:Open b:Back q:Quit "
230
+ elif ui.mode == 'generating':
231
+ footer = " Generating... "
232
+ else:
233
+ footer = " j/k:Nav e:Edit Enter:Generate g:Gallery q:Quit "
234
+ out.append("\033[" + str(height) + ";1H\033[K\033[7m" + footer.ljust(width) + "\033[0m")
235
+
236
+ sys.stdout.write(''.join(out))
237
+ sys.stdout.flush()
238
+
239
+ # ========== Input Handling ==========
240
+ def handle_input(c, fd):
241
+ if ui.mode == 'editing':
242
+ return handle_edit(c, fd)
243
+ if ui.mode == 'gallery':
244
+ return handle_gallery(c, fd)
245
+
246
+ if c == '\x1b':
247
+ if select.select([fd], [], [], 0.05)[0]:
248
+ c2 = os.read(fd, 1).decode('latin-1')
249
+ if c2 == '[':
250
+ c3 = os.read(fd, 1).decode('latin-1')
251
+ if c3 == 'A':
252
+ ui.sel = max(0, ui.sel - 1)
253
+ elif c3 == 'B':
254
+ ui.sel = min(len(ui.params) - 1, ui.sel + 1)
255
+ return True
256
+
257
+ if c == 'q':
258
+ return False
259
+ elif c == 'j':
260
+ ui.sel = min(len(ui.params) - 1, ui.sel + 1)
261
+ elif c == 'k':
262
+ ui.sel = max(0, ui.sel - 1)
263
+ elif c == 'e' or c in ('\r', '\n'):
264
+ if c in ('\r', '\n') and ui.sel == 0 and ui.prompt:
265
+ # Enter on prompt with existing prompt = generate
266
+ generate_videos()
267
+ else:
268
+ ui.mode = 'editing'
269
+ ui.edit_buf = get_param_value(ui.sel)
270
+ if ui.edit_buf in ('(default)', '(auto)'):
271
+ ui.edit_buf = ""
272
+ ui.edit_cursor = len(ui.edit_buf)
273
+ elif c == 'g':
274
+ ui.mode = 'gallery'
275
+ ui.gallery_sel = 0
276
+ ui.gallery_scroll = 0
277
+ elif c == 'G':
278
+ generate_videos()
279
+
280
+ return True
281
+
282
+ def handle_edit(c, fd):
283
+ if c == '\x1b':
284
+ if select.select([fd], [], [], 0.05)[0]:
285
+ os.read(fd, 2)
286
+ ui.mode = 'params'
287
+ return True
288
+
289
+ if c in ('\r', '\n'):
290
+ set_param_value(ui.sel, ui.edit_buf)
291
+ ui.mode = 'params'
292
+ return True
293
+
294
+ if c == '\x7f' or c == '\x08':
295
+ if ui.edit_cursor > 0:
296
+ ui.edit_buf = ui.edit_buf[:ui.edit_cursor-1] + ui.edit_buf[ui.edit_cursor:]
297
+ ui.edit_cursor -= 1
298
+ elif c >= ' ' and c <= '~':
299
+ ui.edit_buf = ui.edit_buf[:ui.edit_cursor] + c + ui.edit_buf[ui.edit_cursor:]
300
+ ui.edit_cursor += 1
301
+
302
+ return True
303
+
304
+ def handle_gallery(c, fd):
305
+ if c == '\x1b':
306
+ if select.select([fd], [], [], 0.05)[0]:
307
+ c2 = os.read(fd, 1).decode('latin-1')
308
+ if c2 == '[':
309
+ c3 = os.read(fd, 1).decode('latin-1')
310
+ if c3 == 'A':
311
+ ui.gallery_sel = max(0, ui.gallery_sel - 1)
312
+ elif c3 == 'B':
313
+ ui.gallery_sel = min(max(0, len(ui.gallery) - 1), ui.gallery_sel + 1)
314
+ else:
315
+ ui.mode = 'params'
316
+ return True
317
+
318
+ if c == 'q':
319
+ return False
320
+ elif c == 'b':
321
+ ui.mode = 'params'
322
+ elif c == 'j':
323
+ ui.gallery_sel = min(max(0, len(ui.gallery) - 1), ui.gallery_sel + 1)
324
+ elif c == 'k':
325
+ ui.gallery_sel = max(0, ui.gallery_sel - 1)
326
+ elif c == 'o' and ui.gallery:
327
+ import subprocess
328
+ path = ui.gallery[ui.gallery_sel]['path']
329
+ try:
330
+ subprocess.Popen(['xdg-open', path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
331
+ ui.status = "Opened: " + os.path.basename(path)
332
+ except:
333
+ ui.status = "Could not open video"
334
+
335
+ # Keep gallery_scroll in range
336
+ _, height = get_size()
337
+ gallery_h = height - 6
338
+ if ui.gallery_sel < ui.gallery_scroll:
339
+ ui.gallery_scroll = ui.gallery_sel
340
+ elif ui.gallery_sel >= ui.gallery_scroll + gallery_h:
341
+ ui.gallery_scroll = ui.gallery_sel - gallery_h + 1
342
+
343
+ return True
344
+
345
+ # ========== One-shot mode ==========
346
+ if ui.prompt and context.get('prompt'):
347
+ # If prompt provided via CLI args, generate immediately
348
+ generate_videos()
349
+ context['output'] = ui.status
350
+ context['messages'] = context.get('messages', [])
351
+
352
+ # ========== Main TUI Loop ==========
353
+ elif not sys.stdin.isatty():
354
+ context['output'] = "Roll requires an interactive terminal."
355
+ else:
356
+ fd = sys.stdin.fileno()
357
+ old_settings = termios.tcgetattr(fd)
358
+
359
+ try:
360
+ tty.setcbreak(fd)
361
+ sys.stdout.write('\033[?25l')
362
+ sys.stdout.write('\033[2J')
363
+ render_screen()
364
+
365
+ running = True
366
+ while running:
367
+ c = os.read(fd, 1).decode('latin-1')
368
+ running = handle_input(c, fd)
369
+ render_screen()
370
+
371
+ finally:
372
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
373
+ sys.stdout.write('\033[?25h')
374
+ sys.stdout.write('\033[2J\033[H')
375
+ sys.stdout.flush()
376
+
377
+ context['output'] = ui.status
378
+ context['messages'] = context.get('messages', [])