npcsh 1.1.22__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 (172) hide show
  1. npcsh/_state.py +272 -120
  2. npcsh/benchmark/npcsh_agent.py +77 -240
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
  4. npcsh/config.py +5 -2
  5. npcsh/npc_team/alicanto.npc +4 -8
  6. npcsh/npc_team/corca.npc +5 -11
  7. npcsh/npc_team/frederic.npc +4 -6
  8. npcsh/npc_team/guac.npc +4 -4
  9. npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
  10. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  12. npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
  13. npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
  14. npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
  15. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
  16. npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
  17. npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
  18. npcsh/npc_team/jinxs/modes/convene.jinx +76 -3
  19. npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
  20. npcsh/npc_team/jinxs/modes/plonk.jinx +76 -14
  21. npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
  22. npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
  23. npcsh/npc_team/jinxs/modes/yap.jinx +504 -30
  24. npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
  25. npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
  26. npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
  27. npcsh/npc_team/kadiefa.npc +4 -5
  28. npcsh/npc_team/npcsh.ctx +16 -0
  29. npcsh/npc_team/plonk.npc +5 -9
  30. npcsh/npc_team/sibiji.npc +13 -5
  31. npcsh/npcsh.py +1 -0
  32. npcsh/routes.py +0 -4
  33. npcsh/yap.py +22 -4
  34. npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
  35. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +4 -8
  36. npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
  37. npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
  38. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -11
  39. npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
  40. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
  41. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/edit_file.jinx +1 -1
  42. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +4 -6
  43. npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
  44. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
  45. npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
  46. npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
  47. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
  48. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +4 -5
  49. npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
  50. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +76 -14
  51. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
  52. npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
  53. npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
  54. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
  55. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +13 -5
  56. npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
  57. npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
  58. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.jinx +504 -30
  59. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/METADATA +168 -7
  60. npcsh-1.1.23.dist-info/RECORD +216 -0
  61. npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
  62. npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
  63. npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
  64. npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
  65. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
  66. npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
  67. npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
  68. npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
  69. npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
  70. npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
  71. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
  72. npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
  73. npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
  74. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
  75. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
  76. npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
  77. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
  78. npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
  79. npcsh-1.1.22.data/data/npcsh/npc_team/add_tab.jinx +0 -11
  80. npcsh-1.1.22.data/data/npcsh/npc_team/close_pane.jinx +0 -9
  81. npcsh-1.1.22.data/data/npcsh/npc_team/close_tab.jinx +0 -10
  82. npcsh-1.1.22.data/data/npcsh/npc_team/confirm.jinx +0 -10
  83. npcsh-1.1.22.data/data/npcsh/npc_team/convene.jinx +0 -232
  84. npcsh-1.1.22.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
  85. npcsh-1.1.22.data/data/npcsh/npc_team/help.jinx +0 -52
  86. npcsh-1.1.22.data/data/npcsh/npc_team/init.jinx +0 -41
  87. npcsh-1.1.22.data/data/npcsh/npc_team/list_panes.jinx +0 -8
  88. npcsh-1.1.22.data/data/npcsh/npc_team/navigate.jinx +0 -10
  89. npcsh-1.1.22.data/data/npcsh/npc_team/notify.jinx +0 -10
  90. npcsh-1.1.22.data/data/npcsh/npc_team/npcsh.ctx +0 -18
  91. npcsh-1.1.22.data/data/npcsh/npc_team/open_pane.jinx +0 -13
  92. npcsh-1.1.22.data/data/npcsh/npc_team/read_pane.jinx +0 -9
  93. npcsh-1.1.22.data/data/npcsh/npc_team/roll.jinx +0 -65
  94. npcsh-1.1.22.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
  95. npcsh-1.1.22.data/data/npcsh/npc_team/send_message.jinx +0 -10
  96. npcsh-1.1.22.data/data/npcsh/npc_team/serve.jinx +0 -26
  97. npcsh-1.1.22.data/data/npcsh/npc_team/split_pane.jinx +0 -12
  98. npcsh-1.1.22.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
  99. npcsh-1.1.22.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
  100. npcsh-1.1.22.data/data/npcsh/npc_team/write_file.jinx +0 -11
  101. npcsh-1.1.22.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
  102. npcsh-1.1.22.dist-info/RECORD +0 -240
  103. /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
  104. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
  105. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
  106. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  107. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  108. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  109. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  110. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/build.jinx +0 -0
  111. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
  112. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
  113. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  114. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  115. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
  116. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
  117. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.jinx +0 -0
  118. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
  119. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
  120. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  121. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  122. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
  123. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
  124. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
  125. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
  126. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  127. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  128. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  129. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +0 -0
  130. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  131. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  132. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
  133. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
  134. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  135. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
  136. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  137. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
  138. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
  139. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
  140. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
  141. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  142. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
  143. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
  144. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  145. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
  146. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  147. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
  148. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
  149. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
  150. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  152. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
  153. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
  154. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
  155. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
  156. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
  159. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  160. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  161. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  162. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
  163. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  164. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  165. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
  166. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
  167. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  168. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
  169. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
  170. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
  171. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
  172. {npcsh-1.1.22.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
@@ -136,26 +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"
146
- prompt += "IMPORTANT: Before taking new actions, VERIFY the current screenshot shows the expected result of your previous actions. "
147
- prompt += "If the page is still loading or hasn't changed yet, use 'wait' with duration 2-3 seconds. "
148
- prompt += "Do NOT blindly proceed confirm each step worked before moving on.\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"
149
185
  prompt += "TASK: " + task['text'] + "\n"
150
186
  prompt += history_context + "\n"
151
187
  prompt += "Available actions:\n"
152
- prompt += "- click: Click at x,y coordinates (0-100 percentage of screen)\n"
153
- prompt += "- type: Type text (use 'text' field)\n"
154
- prompt += "- key: Press key like enter, tab, escape (use 'text' field)\n"
155
- prompt += "- launch: Launch application (use 'command' field, e.g. " + app_examples + ")\n"
156
- prompt += "- wait: Wait for 'duration' seconds (use when page is loading or UI hasn't updated)\n"
157
- prompt += "- done: Task completed successfully\n"
158
- 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"
159
195
  prompt += "Respond with JSON, e.g.: " + json_schema_example
160
196
 
161
197
  ui.status = "Thinking... (iter " + str(ui.iteration) + "/" + str(ui.max_iter) + ")"
@@ -176,21 +212,38 @@ steps:
176
212
  action_response = json.loads(action_response)
177
213
  except:
178
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")
179
219
  ui.status = "Bad response, retrying..."
180
220
  return
181
221
 
182
222
  action = action_response.get('action', 'fail')
183
223
  reason = action_response.get('reason', '')
184
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
+
185
232
  if action == 'done':
186
233
  task['status'] = 'done'
187
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)
188
238
  advance_to_next_task()
189
239
  return
190
240
 
191
241
  if action == 'fail':
192
242
  task['status'] = 'failed'
193
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)
194
247
  advance_to_next_task()
195
248
  return
196
249
 
@@ -208,11 +261,16 @@ steps:
208
261
  act_record['text'] = txt
209
262
  elif action == 'key':
210
263
  key = action_response.get('text', 'enter')
211
- perform_action({"type": "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})
212
270
  act_record['key'] = key
213
271
  elif action == 'launch':
214
272
  cmd = action_response.get('command', '')
215
- perform_action({"type": "launch", "command": cmd})
273
+ perform_action({"type": "shell", "command": cmd})
216
274
  act_record['command'] = cmd
217
275
  time.sleep(2)
218
276
  elif action == 'wait':
@@ -236,6 +294,10 @@ steps:
236
294
 
237
295
  except Exception as e:
238
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))
239
301
  ui.status = "Error: " + str(e)[:40]
240
302
 
241
303
  def advance_to_next_task():
@@ -372,7 +434,7 @@ steps:
372
434
  if a.get('x') is not None:
373
435
  coords = "(" + str(a.get('x', '')) + "," + str(a.get('y', '')) + ") "
374
436
  elif a.get('text'):
375
- coords = '"' + str(a['text'])[:15] + '" '
437
+ coords = '"' + str(a['text'])[:40] + '" '
376
438
  elif a.get('key'):
377
439
  coords = '[' + str(a['key']) + '] '
378
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', [])