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,7 +1,11 @@
1
1
  jinx_name: kg
2
2
  description: Interactive knowledge graph browser - explore facts, concepts, and links
3
3
  interactive: true
4
- inputs: []
4
+ inputs:
5
+ - action: ""
6
+ - dream: false
7
+ - backfill: false
8
+ - ops: ""
5
9
  steps:
6
10
  - name: kg_browser
7
11
  engine: python
@@ -12,7 +16,70 @@ steps:
12
16
  import termios
13
17
  import select
14
18
 
15
- if not sys.stdin.isatty():
19
+ _kg_action = (context.get('action') or '').strip().lower()
20
+
21
+ if _kg_action in ('sleep', 'evolve', 'dream'):
22
+ # Route to KG evolution operations
23
+ import traceback
24
+ from npcpy.memory.command_history import CommandHistory, load_kg_from_db, save_kg_to_db
25
+ from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process, kg_backfill_from_memories
26
+
27
+ _npc = context.get('npc')
28
+ _team = context.get('team')
29
+ _msgs = context.get('messages', [])
30
+ _do_dream = _kg_action == 'dream' or str(context.get('dream', '')).lower() in ('true', '1', 'yes')
31
+ _do_backfill = str(context.get('backfill', '')).lower() in ('true', '1', 'yes')
32
+ _ops_str = context.get('ops', '')
33
+ _ops_config = [op.strip() for op in _ops_str.split(',') if op.strip()] if _ops_str else None
34
+
35
+ _model = (_npc.model if _npc and hasattr(_npc, 'model') else None) or (state.chat_model if state else 'llama3.2')
36
+ _provider = (_npc.provider if _npc and hasattr(_npc, 'provider') else None) or (state.chat_provider if state else 'ollama')
37
+
38
+ _team_name = _team.name if _team else '__none__'
39
+ _npc_name = _npc.name if _npc else '__none__'
40
+ _cur_path = os.getcwd()
41
+
42
+ try:
43
+ _db_path = os.getenv('NPCSH_DB_PATH', os.path.expanduser('~/npcsh_history.db'))
44
+ _ch = CommandHistory(_db_path)
45
+ _eng = _ch.engine
46
+
47
+ _result = ''
48
+ if _do_backfill:
49
+ print('Backfilling from approved memories...')
50
+ _stats = kg_backfill_from_memories(_eng, model=_model, provider=_provider, npc=_npc, get_concepts=True, dry_run=False)
51
+ _result += f"Backfill: +{_stats['facts_after'] - _stats['facts_before']} facts, +{_stats['concepts_after'] - _stats['concepts_before']} concepts\n"
52
+
53
+ _kg = load_kg_from_db(_eng, _team_name, _npc_name, _cur_path)
54
+ if not _kg or not _kg.get('facts'):
55
+ context['output'] = _result + 'Knowledge graph is empty. Use /kg backfill=true or have conversations first.'
56
+ context['messages'] = _msgs
57
+ _ch.close()
58
+ exit()
59
+
60
+ _f0 = len(_kg.get('facts', []))
61
+ _c0 = len(_kg.get('concepts', []))
62
+ _label = 'Sleep'
63
+
64
+ _kg, _ = kg_sleep_process(existing_kg=_kg, model=_model, provider=_provider, npc=_npc, operations_config=_ops_config)
65
+
66
+ if _do_dream:
67
+ _label += ' & Dream'
68
+ _kg, _ = kg_dream_process(existing_kg=_kg, model=_model, provider=_provider, npc=_npc)
69
+
70
+ save_kg_to_db(_eng, _kg, _team_name, _npc_name, _cur_path)
71
+ _f1 = len(_kg.get('facts', []))
72
+ _c1 = len(_kg.get('concepts', []))
73
+ _result += f"{_label} complete. Facts: {_f0} -> {_f1} ({_f1-_f0:+}), Concepts: {_c0} -> {_c1} ({_c1-_c0:+})"
74
+ context['output'] = _result
75
+ context['messages'] = _msgs
76
+ _ch.close()
77
+ except Exception as e:
78
+ traceback.print_exc()
79
+ context['output'] = f'KG evolution error: {e}'
80
+ context['messages'] = _msgs
81
+
82
+ elif not sys.stdin.isatty():
16
83
  context['output'] = "KG browser requires an interactive terminal."
17
84
 
18
85
  else:
@@ -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'):