npcsh 1.1.14__py3-none-any.whl → 1.1.16__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 (168) hide show
  1. npcsh/_state.py +533 -80
  2. npcsh/mcp_server.py +2 -1
  3. npcsh/npc.py +84 -32
  4. npcsh/npc_team/alicanto.npc +22 -1
  5. npcsh/npc_team/corca.npc +28 -9
  6. npcsh/npc_team/frederic.npc +25 -4
  7. npcsh/npc_team/guac.npc +22 -0
  8. npcsh/npc_team/jinxs/bin/nql.jinx +141 -0
  9. npcsh/npc_team/jinxs/bin/sync.jinx +230 -0
  10. {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/bin}/vixynt.jinx +8 -30
  11. npcsh/npc_team/jinxs/bin/wander.jinx +152 -0
  12. npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +220 -0
  13. npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +40 -0
  14. npcsh/npc_team/jinxs/lib/browser/close_browser.jinx +14 -0
  15. npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +43 -0
  16. npcsh/npc_team/jinxs/lib/computer_use/click.jinx +23 -0
  17. npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +26 -0
  18. npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +37 -0
  19. npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +23 -0
  20. npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +27 -0
  21. npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +21 -0
  22. {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/edit_file.jinx +3 -3
  23. {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/load_file.jinx +1 -1
  24. npcsh/npc_team/jinxs/lib/core/paste.jinx +134 -0
  25. {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/search.jinx +2 -1
  26. npcsh/npc_team/jinxs/{code → lib/core}/sh.jinx +2 -8
  27. npcsh/npc_team/jinxs/{code → lib/core}/sql.jinx +1 -1
  28. npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +232 -0
  29. npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +184 -0
  30. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +76 -0
  31. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +101 -0
  32. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +69 -0
  33. npcsh/npc_team/jinxs/{utils/core → lib/utils}/build.jinx +8 -8
  34. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +176 -0
  35. npcsh/npc_team/jinxs/lib/utils/shh.jinx +17 -0
  36. npcsh/npc_team/jinxs/lib/utils/switch.jinx +62 -0
  37. npcsh/npc_team/jinxs/lib/utils/switches.jinx +61 -0
  38. npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +205 -0
  39. npcsh/npc_team/jinxs/lib/utils/verbose.jinx +17 -0
  40. npcsh/npc_team/kadiefa.npc +19 -1
  41. npcsh/npc_team/plonk.npc +26 -1
  42. npcsh/npc_team/plonkjr.npc +22 -1
  43. npcsh/npc_team/sibiji.npc +23 -2
  44. npcsh/npcsh.py +153 -39
  45. npcsh/ui.py +22 -1
  46. npcsh-1.1.16.data/data/npcsh/npc_team/alicanto.npc +23 -0
  47. npcsh-1.1.16.data/data/npcsh/npc_team/arxiv.jinx +76 -0
  48. npcsh-1.1.16.data/data/npcsh/npc_team/browser_action.jinx +220 -0
  49. npcsh-1.1.16.data/data/npcsh/npc_team/browser_screenshot.jinx +40 -0
  50. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/build.jinx +8 -8
  51. npcsh-1.1.16.data/data/npcsh/npc_team/click.jinx +23 -0
  52. npcsh-1.1.16.data/data/npcsh/npc_team/close_browser.jinx +14 -0
  53. npcsh-1.1.16.data/data/npcsh/npc_team/convene.jinx +232 -0
  54. npcsh-1.1.16.data/data/npcsh/npc_team/corca.npc +31 -0
  55. npcsh-1.1.16.data/data/npcsh/npc_team/delegate.jinx +184 -0
  56. {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/edit_file.jinx +3 -3
  57. npcsh-1.1.16.data/data/npcsh/npc_team/frederic.npc +27 -0
  58. npcsh-1.1.16.data/data/npcsh/npc_team/guac.npc +22 -0
  59. npcsh-1.1.16.data/data/npcsh/npc_team/jinxs.jinx +176 -0
  60. npcsh-1.1.16.data/data/npcsh/npc_team/kadiefa.npc +21 -0
  61. npcsh-1.1.16.data/data/npcsh/npc_team/key_press.jinx +26 -0
  62. npcsh-1.1.16.data/data/npcsh/npc_team/launch_app.jinx +37 -0
  63. {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/load_file.jinx +1 -1
  64. npcsh-1.1.16.data/data/npcsh/npc_team/nql.jinx +141 -0
  65. npcsh-1.1.16.data/data/npcsh/npc_team/open_browser.jinx +43 -0
  66. npcsh-1.1.16.data/data/npcsh/npc_team/paper_search.jinx +101 -0
  67. npcsh-1.1.16.data/data/npcsh/npc_team/paste.jinx +134 -0
  68. npcsh-1.1.16.data/data/npcsh/npc_team/plonk.npc +27 -0
  69. npcsh-1.1.16.data/data/npcsh/npc_team/plonkjr.npc +23 -0
  70. npcsh-1.1.16.data/data/npcsh/npc_team/screenshot.jinx +23 -0
  71. {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/search.jinx +2 -1
  72. npcsh-1.1.16.data/data/npcsh/npc_team/semantic_scholar.jinx +69 -0
  73. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sh.jinx +2 -8
  74. npcsh-1.1.16.data/data/npcsh/npc_team/shh.jinx +17 -0
  75. npcsh-1.1.16.data/data/npcsh/npc_team/sibiji.npc +24 -0
  76. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sql.jinx +1 -1
  77. npcsh-1.1.16.data/data/npcsh/npc_team/switch.jinx +62 -0
  78. npcsh-1.1.16.data/data/npcsh/npc_team/switches.jinx +61 -0
  79. npcsh-1.1.16.data/data/npcsh/npc_team/sync.jinx +230 -0
  80. npcsh-1.1.16.data/data/npcsh/npc_team/teamviz.jinx +205 -0
  81. npcsh-1.1.16.data/data/npcsh/npc_team/type_text.jinx +27 -0
  82. npcsh-1.1.16.data/data/npcsh/npc_team/verbose.jinx +17 -0
  83. {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/vixynt.jinx +8 -30
  84. npcsh-1.1.16.data/data/npcsh/npc_team/wait.jinx +21 -0
  85. npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +152 -0
  86. {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/METADATA +399 -58
  87. npcsh-1.1.16.dist-info/RECORD +170 -0
  88. npcsh-1.1.16.dist-info/entry_points.txt +19 -0
  89. npcsh-1.1.16.dist-info/top_level.txt +2 -0
  90. project/__init__.py +1 -0
  91. npcsh/npc_team/foreman.npc +0 -7
  92. npcsh/npc_team/jinxs/modes/alicanto.jinx +0 -194
  93. npcsh/npc_team/jinxs/modes/corca.jinx +0 -249
  94. npcsh/npc_team/jinxs/modes/guac.jinx +0 -317
  95. npcsh/npc_team/jinxs/modes/plonk.jinx +0 -214
  96. npcsh/npc_team/jinxs/modes/pti.jinx +0 -170
  97. npcsh/npc_team/jinxs/modes/wander.jinx +0 -186
  98. npcsh/npc_team/jinxs/utils/agent.jinx +0 -17
  99. npcsh/npc_team/jinxs/utils/core/jinxs.jinx +0 -32
  100. npcsh-1.1.14.data/data/npcsh/npc_team/agent.jinx +0 -17
  101. npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.jinx +0 -194
  102. npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.npc +0 -2
  103. npcsh-1.1.14.data/data/npcsh/npc_team/corca.jinx +0 -249
  104. npcsh-1.1.14.data/data/npcsh/npc_team/corca.npc +0 -12
  105. npcsh-1.1.14.data/data/npcsh/npc_team/foreman.npc +0 -7
  106. npcsh-1.1.14.data/data/npcsh/npc_team/frederic.npc +0 -6
  107. npcsh-1.1.14.data/data/npcsh/npc_team/guac.jinx +0 -317
  108. npcsh-1.1.14.data/data/npcsh/npc_team/jinxs.jinx +0 -32
  109. npcsh-1.1.14.data/data/npcsh/npc_team/kadiefa.npc +0 -3
  110. npcsh-1.1.14.data/data/npcsh/npc_team/plonk.jinx +0 -214
  111. npcsh-1.1.14.data/data/npcsh/npc_team/plonk.npc +0 -2
  112. npcsh-1.1.14.data/data/npcsh/npc_team/plonkjr.npc +0 -2
  113. npcsh-1.1.14.data/data/npcsh/npc_team/pti.jinx +0 -170
  114. npcsh-1.1.14.data/data/npcsh/npc_team/sibiji.npc +0 -3
  115. npcsh-1.1.14.data/data/npcsh/npc_team/wander.jinx +0 -186
  116. npcsh-1.1.14.dist-info/RECORD +0 -135
  117. npcsh-1.1.14.dist-info/entry_points.txt +0 -9
  118. npcsh-1.1.14.dist-info/top_level.txt +0 -1
  119. /npcsh/npc_team/jinxs/{utils → bin}/roll.jinx +0 -0
  120. /npcsh/npc_team/jinxs/{utils → bin}/sample.jinx +0 -0
  121. /npcsh/npc_team/jinxs/{modes → bin}/spool.jinx +0 -0
  122. /npcsh/npc_team/jinxs/{modes → bin}/yap.jinx +0 -0
  123. /npcsh/npc_team/jinxs/{utils → lib/computer_use}/trigger.jinx +0 -0
  124. /npcsh/npc_team/jinxs/{utils → lib/core}/chat.jinx +0 -0
  125. /npcsh/npc_team/jinxs/{utils → lib/core}/cmd.jinx +0 -0
  126. /npcsh/npc_team/jinxs/{utils → lib/core}/compress.jinx +0 -0
  127. /npcsh/npc_team/jinxs/{utils → lib/core}/ots.jinx +0 -0
  128. /npcsh/npc_team/jinxs/{code → lib/core}/python.jinx +0 -0
  129. /npcsh/npc_team/jinxs/{utils → lib/core}/sleep.jinx +0 -0
  130. /npcsh/npc_team/jinxs/{utils/core → lib/utils}/compile.jinx +0 -0
  131. /npcsh/npc_team/jinxs/{utils/core → lib/utils}/help.jinx +0 -0
  132. /npcsh/npc_team/jinxs/{utils/core → lib/utils}/init.jinx +0 -0
  133. /npcsh/npc_team/jinxs/{utils → lib/utils}/serve.jinx +0 -0
  134. /npcsh/npc_team/jinxs/{utils/core → lib/utils}/set.jinx +0 -0
  135. /npcsh/npc_team/jinxs/{utils → lib/utils}/usage.jinx +0 -0
  136. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/alicanto.png +0 -0
  137. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/chat.jinx +0 -0
  138. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  139. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/compile.jinx +0 -0
  140. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/compress.jinx +0 -0
  141. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/corca.png +0 -0
  142. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/corca_example.png +0 -0
  143. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/frederic4.png +0 -0
  144. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/guac.png +0 -0
  145. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/help.jinx +0 -0
  146. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/init.jinx +0 -0
  147. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  148. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
  149. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  150. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  151. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/ots.jinx +0 -0
  152. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/plonk.png +0 -0
  153. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  154. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/python.jinx +0 -0
  155. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/roll.jinx +0 -0
  156. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sample.jinx +0 -0
  157. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/serve.jinx +0 -0
  158. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/set.jinx +0 -0
  159. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sibiji.png +0 -0
  160. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  161. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/spool.jinx +0 -0
  162. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/spool.png +0 -0
  163. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  164. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/usage.jinx +0 -0
  165. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/yap.jinx +0 -0
  166. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/yap.png +0 -0
  167. {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/WHEEL +0 -0
  168. {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,27 @@
1
+ jinx_name: type_text
2
+ description: Type text using keyboard
3
+ inputs:
4
+ - text: "" # Text to type
5
+
6
+ steps:
7
+ - name: perform_type
8
+ engine: python
9
+ code: |
10
+ from npcpy.work.desktop import perform_action
11
+
12
+ text = context.get('text', '')
13
+ messages = context.get('messages', [])
14
+
15
+ if not text:
16
+ context['output'] = "Usage: /type_text <text to type>"
17
+ context['messages'] = messages
18
+ exit()
19
+
20
+ try:
21
+ perform_action({'type': 'type', 'text': text})
22
+ preview = text[:30] + "..." if len(text) > 30 else text
23
+ context['output'] = f"Typed: {preview}"
24
+ except Exception as e:
25
+ context['output'] = f"Type failed: {e}"
26
+
27
+ context['messages'] = messages
@@ -0,0 +1,21 @@
1
+ jinx_name: wait
2
+ description: Wait/pause for a specified duration in seconds
3
+ inputs:
4
+ - duration: 1 # Duration to wait in seconds
5
+
6
+ steps:
7
+ - name: perform_wait
8
+ engine: python
9
+ code: |
10
+ import time
11
+
12
+ duration = float(context.get('duration', 1))
13
+ messages = context.get('messages', [])
14
+
15
+ try:
16
+ time.sleep(duration)
17
+ context['output'] = f"Waited {duration} seconds"
18
+ except Exception as e:
19
+ context['output'] = f"Wait failed: {e}"
20
+
21
+ context['messages'] = messages
@@ -13,9 +13,9 @@ steps:
13
13
  from npcpy.llm_funcs import get_llm_response
14
14
 
15
15
 
16
- file_path = os.path.expanduser("{{ file_path }}")
17
- edit_instructions = "{{ edit_instructions }}"
18
- backup_str = "{{ backup }}"
16
+ file_path = os.path.expanduser({{ file_path | tojson }})
17
+ edit_instructions = {{ edit_instructions | string | tojson }}
18
+ backup_str = {{ backup | default("true") | string | tojson }}
19
19
  create_backup = backup_str.lower() not in ('false', 'no', '0', '')
20
20
 
21
21
 
@@ -10,7 +10,7 @@ steps:
10
10
  from npcpy.data.load import load_file_contents
11
11
 
12
12
  # Expand user path and get absolute path
13
- file_path = os.path.expanduser("{{ file_path }}")
13
+ file_path = os.path.expanduser({{ file_path | tojson }})
14
14
 
15
15
  # Check if file exists
16
16
  if not os.path.exists(file_path):
@@ -0,0 +1,134 @@
1
+ jinx_name: paste
2
+ description: Grabs content from clipboard (images or text) and saves/displays it. Use this when Ctrl+V paste doesn't work properly.
3
+ inputs:
4
+ - output_path:
5
+ default: ""
6
+ description: "Optional path to save image to. If empty, saves to temp file."
7
+ steps:
8
+ - name: "paste_clipboard"
9
+ engine: "python"
10
+ code: |
11
+ import tempfile
12
+ import os
13
+ from datetime import datetime
14
+ import io
15
+
16
+ output_path = ({{ output_path | default("") | tojson }} or "").strip()
17
+ image_saved = False
18
+ text_content = None
19
+
20
+ # Try PIL/Pillow with ImageGrab first (works on most systems)
21
+ try:
22
+ from PIL import ImageGrab, Image
23
+
24
+ # Try to grab image from clipboard
25
+ img = ImageGrab.grabclipboard()
26
+
27
+ if img is not None:
28
+ if isinstance(img, Image.Image):
29
+ # It's an image
30
+ if output_path:
31
+ save_path = os.path.expanduser(output_path)
32
+ else:
33
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
34
+ fd, save_path = tempfile.mkstemp(suffix='.png', prefix=f'npcsh_paste_{timestamp}_')
35
+ os.close(fd)
36
+
37
+ img.save(save_path, 'PNG')
38
+ file_size = os.path.getsize(save_path)
39
+
40
+ if file_size > 1024 * 1024:
41
+ size_str = f"{file_size / (1024*1024):.1f} MB"
42
+ elif file_size > 1024:
43
+ size_str = f"{file_size / 1024:.1f} KB"
44
+ else:
45
+ size_str = f"{file_size} bytes"
46
+
47
+ output = f"Image saved to: {save_path} ({size_str}, {img.size[0]}x{img.size[1]})"
48
+ context['pasted_image_path'] = save_path
49
+ image_saved = True
50
+
51
+ elif isinstance(img, list):
52
+ # It's a list of file paths (copied files)
53
+ output = f"Clipboard contains {len(img)} file(s):\n" + "\n".join(img)
54
+ context['pasted_files'] = img
55
+ image_saved = True
56
+
57
+ except ImportError:
58
+ pass
59
+ except Exception as e:
60
+ # ImageGrab failed, try other methods
61
+ pass
62
+
63
+ # If no image, try to get text via tkinter
64
+ if not image_saved:
65
+ try:
66
+ import tkinter as tk
67
+
68
+ root = tk.Tk()
69
+ root.withdraw()
70
+
71
+ try:
72
+ text_content = root.clipboard_get()
73
+ except tk.TclError:
74
+ text_content = None
75
+
76
+ root.destroy()
77
+
78
+ except Exception as e:
79
+ pass
80
+
81
+ # If still nothing, try GTK
82
+ if not image_saved and text_content is None:
83
+ try:
84
+ import gi
85
+ gi.require_version('Gtk', '3.0')
86
+ from gi.repository import Gtk, Gdk, GdkPixbuf
87
+
88
+ clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
89
+
90
+ # Try image first
91
+ pixbuf = clipboard.wait_for_image()
92
+ if pixbuf:
93
+ if output_path:
94
+ save_path = os.path.expanduser(output_path)
95
+ else:
96
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
97
+ fd, save_path = tempfile.mkstemp(suffix='.png', prefix=f'npcsh_paste_{timestamp}_')
98
+ os.close(fd)
99
+
100
+ pixbuf.savev(save_path, 'png', [], [])
101
+ file_size = os.path.getsize(save_path)
102
+
103
+ if file_size > 1024 * 1024:
104
+ size_str = f"{file_size / (1024*1024):.1f} MB"
105
+ elif file_size > 1024:
106
+ size_str = f"{file_size / 1024:.1f} KB"
107
+ else:
108
+ size_str = f"{file_size} bytes"
109
+
110
+ output = f"Image saved to: {save_path} ({size_str}, {pixbuf.get_width()}x{pixbuf.get_height()})"
111
+ context['pasted_image_path'] = save_path
112
+ image_saved = True
113
+ else:
114
+ # Try text
115
+ text_content = clipboard.wait_for_text()
116
+
117
+ except Exception as e:
118
+ pass
119
+
120
+ # Handle text content
121
+ if not image_saved and text_content:
122
+ line_count = text_content.count('\n') + 1
123
+ char_count = len(text_content)
124
+ context['pasted_text'] = text_content
125
+
126
+ if line_count > 10:
127
+ preview_lines = text_content.split('\n')[:10]
128
+ preview = '\n'.join(preview_lines)
129
+ output = f"Clipboard text ({line_count} lines, {char_count} chars):\n---\n{preview}\n... ({line_count - 10} more lines)\n---"
130
+ else:
131
+ output = f"Clipboard text ({line_count} lines, {char_count} chars):\n---\n{text_content}\n---"
132
+
133
+ elif not image_saved:
134
+ output = "Clipboard is empty or could not access clipboard.\nMake sure you have PIL/Pillow installed: pip install Pillow"
@@ -25,7 +25,8 @@ steps:
25
25
  code: |
26
26
  import os
27
27
  import traceback
28
-
28
+ from npcpy.data.web import search_web
29
+
29
30
  # Access query from context
30
31
  query = context.get('query')
31
32
  if not query or not query.strip():
@@ -1,5 +1,5 @@
1
1
  jinx_name: sh
2
- description: Execute bash queries. Should be used to grep for file contents, list directories, explore information to answer user questions more practically.
2
+ description: Execute bash queries. Should be used to grep for file contents, list directories, explore information to answer user questions more practically. NEVER use ls -R on directories that may contain node_modules, .git, or other large dependency folders - this will exceed token limits. Use targeted ls commands instead.
3
3
  inputs:
4
4
  - bash_command
5
5
  steps:
@@ -9,7 +9,7 @@ steps:
9
9
  import subprocess
10
10
  import os
11
11
 
12
- cmd = '{{ bash_command }}'
12
+ cmd = {{ bash_command | tojson }}
13
13
  output = ""
14
14
 
15
15
  process = subprocess.Popen(
@@ -20,12 +20,6 @@ steps:
20
20
  )
21
21
  stdout, stderr = process.communicate()
22
22
 
23
- # Only show debug output if NPCSH_DEBUG is set
24
- if os.environ.get("NPCSH_DEBUG") == "1":
25
- import sys
26
- print(f"[sh] cmd: {cmd}", file=sys.stderr)
27
- print(f"[sh] stdout: {stdout.decode('utf-8', errors='ignore')[:200]}", file=sys.stderr)
28
-
29
23
  if stderr:
30
24
  output = f"Error: {stderr.decode('utf-8')}"
31
25
  else:
@@ -8,7 +8,7 @@ steps:
8
8
  - engine: python
9
9
  code: |
10
10
  import pandas as pd
11
- query = "{{ sql_query }}"
11
+ query = {{ sql_query | tojson }}
12
12
  try:
13
13
  df = pd.read_sql_query(query, npc.db_conn)
14
14
  except Exception as e:
@@ -0,0 +1,232 @@
1
+ jinx_name: convene
2
+ description: Run a cycle of discussions between NPCs on a topic. The orchestrator convenes agents to discuss and synthesize.
3
+ inputs:
4
+ - topic: ""
5
+ - npcs: "alicanto,corca,guac"
6
+ - rounds: 3
7
+ - model: null
8
+ - provider: null
9
+ steps:
10
+ - name: convene_discussion
11
+ engine: python
12
+ code: |
13
+ from termcolor import colored
14
+ from npcpy.llm_funcs import get_llm_response
15
+
16
+ topic = context.get('topic', '')
17
+ npcs_str = context.get('npcs', 'alicanto,corca,guac')
18
+ rounds = int(context.get('rounds', 3))
19
+
20
+ npc = context.get('npc')
21
+ team = context.get('team')
22
+ messages = context.get('messages', [])
23
+
24
+ model = context.get('model') or (npc.model if npc else 'gemini-1.5-flash')
25
+ provider = context.get('provider') or (npc.provider if npc else 'gemini')
26
+
27
+ if not topic:
28
+ context['output'] = """Usage: /convene <topic>
29
+
30
+ Options:
31
+ --npcs LIST Comma-separated NPC names (default: alicanto,corca,guac)
32
+ --rounds N Number of discussion rounds (default: 3)
33
+
34
+ Example: /convene "How should we approach the database migration?" --npcs corca,guac,frederic
35
+ """
36
+ exit()
37
+
38
+ npc_names = [n.strip() for n in npcs_str.split(',')]
39
+
40
+ print(f"""
41
+ ██████ ██████ ███ ██ ██ ██ ███████ ███ ██ ███████
42
+ ██ ██ ██ ████ ██ ██ ██ ██ ████ ██ ██
43
+ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ █████
44
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
45
+ ██████ ██████ ██ ████ ████ ███████ ██ ████ ███████
46
+
47
+ Convening Discussion
48
+ Topic: {topic}
49
+ Participants: {', '.join(npc_names)}
50
+ Rounds: {rounds}
51
+ """)
52
+
53
+ # Get NPC personas
54
+ participants = []
55
+ for name in npc_names:
56
+ if team and hasattr(team, 'npcs') and name in team.npcs:
57
+ target_npc = team.npcs[name]
58
+ persona = getattr(target_npc, 'primary_directive', f'{name} specialist')
59
+ participants.append({'name': name, 'persona': persona, 'npc': target_npc})
60
+ else:
61
+ participants.append({'name': name, 'persona': f'{name} - general assistant', 'npc': None})
62
+
63
+ import random
64
+
65
+ discussion_log = []
66
+
67
+ for round_num in range(1, rounds + 1):
68
+ print(colored(f"\n{'='*60}", "cyan"))
69
+ print(colored(f" ROUND {round_num}/{rounds}", "cyan", attrs=["bold"]))
70
+ print(colored(f"{'='*60}", "cyan"))
71
+
72
+ round_contributions = []
73
+
74
+ for participant in participants:
75
+ name = participant['name']
76
+ persona = participant['persona']
77
+
78
+ # Build context from previous contributions
79
+ prev_context = ""
80
+ if discussion_log:
81
+ prev_context = "\n\nPrevious discussion:\n"
82
+ for entry in discussion_log[-len(participants)*2:]:
83
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
84
+
85
+ if round_contributions:
86
+ prev_context += "\nThis round so far:\n"
87
+ for entry in round_contributions:
88
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
89
+
90
+ prompt = f"""You are {name}. {persona}
91
+
92
+ Topic under discussion: "{topic}"
93
+ {prev_context}
94
+
95
+ Provide your perspective on this topic. Be concise but insightful.
96
+ Build on what others have said if applicable.
97
+ If you disagree with something, explain why constructively.
98
+ """
99
+
100
+ print(colored(f"\n[{name}]:", "yellow", attrs=["bold"]))
101
+
102
+ resp = get_llm_response(
103
+ prompt,
104
+ model=model,
105
+ provider=provider,
106
+ npc=participant.get('npc') or npc,
107
+ temperature=0.7
108
+ )
109
+
110
+ contribution = str(resp.get('response', ''))
111
+ print(contribution)
112
+
113
+ entry = {
114
+ 'round': round_num,
115
+ 'speaker': name,
116
+ 'contribution': contribution
117
+ }
118
+ round_contributions.append(entry)
119
+ discussion_log.append(entry)
120
+
121
+ # Sample a followup from another participant
122
+ other_participants = [p for p in participants if p['name'] != name]
123
+ if other_participants:
124
+ followup_participant = random.choice(other_participants)
125
+ followup_name = followup_participant['name']
126
+ followup_persona = followup_participant['persona']
127
+
128
+ followup_prompt = f"""You are {followup_name}. {followup_persona}
129
+
130
+ Topic: "{topic}"
131
+
132
+ {name} just said: "{contribution[:500]}"
133
+
134
+ Respond briefly to this specific point - agree, disagree, build on it, or ask a clarifying question.
135
+ Keep it to 2-3 sentences.
136
+ """
137
+
138
+ print(colored(f"\n [{followup_name} responds]:", "cyan"))
139
+
140
+ followup_resp = get_llm_response(
141
+ followup_prompt,
142
+ model=model,
143
+ provider=provider,
144
+ npc=followup_participant.get('npc') or npc,
145
+ temperature=0.7
146
+ )
147
+
148
+ followup_contribution = str(followup_resp.get('response', ''))
149
+ print(f" {followup_contribution}")
150
+
151
+ discussion_log.append({
152
+ 'round': round_num,
153
+ 'speaker': followup_name,
154
+ 'contribution': followup_contribution,
155
+ 'type': 'followup'
156
+ })
157
+
158
+ # Probability of original speaker responding back vs someone else
159
+ if random.random() < 0.4:
160
+ # Original speaker responds
161
+ responder = participant
162
+ responder_name = name
163
+ else:
164
+ # Sample from others (could be followup person or someone else)
165
+ responder = random.choice(other_participants)
166
+ responder_name = responder['name']
167
+
168
+ if random.random() < 0.6: # 60% chance of a counter-response
169
+ counter_prompt = f"""You are {responder_name}. {responder['persona']}
170
+
171
+ Topic: "{topic}"
172
+
173
+ {followup_name} responded: "{followup_contribution}"
174
+
175
+ Brief reaction (1-2 sentences). Move the discussion forward.
176
+ """
177
+
178
+ print(colored(f"\n [{responder_name}]:", "magenta"))
179
+
180
+ counter_resp = get_llm_response(
181
+ counter_prompt,
182
+ model=model,
183
+ provider=provider,
184
+ npc=responder.get('npc') or npc,
185
+ temperature=0.7
186
+ )
187
+
188
+ counter_contribution = str(counter_resp.get('response', ''))
189
+ print(f" {counter_contribution}")
190
+
191
+ discussion_log.append({
192
+ 'round': round_num,
193
+ 'speaker': responder_name,
194
+ 'contribution': counter_contribution,
195
+ 'type': 'counter'
196
+ })
197
+
198
+ # Synthesis
199
+ print(colored(f"\n{'='*60}", "green"))
200
+ print(colored(" SYNTHESIS", "green", attrs=["bold"]))
201
+ print(colored(f"{'='*60}", "green"))
202
+
203
+ all_contributions = "\n".join([
204
+ f"[{e['speaker']} - Round {e['round']}]: {e['contribution']}"
205
+ for e in discussion_log
206
+ ])
207
+
208
+ synthesis_prompt = f"""As the convener of this discussion on "{topic}", synthesize the key points:
209
+
210
+ Full discussion:
211
+ {all_contributions}
212
+
213
+ Provide:
214
+ 1. Key agreements and consensus points
215
+ 2. Areas of disagreement or tension
216
+ 3. Novel ideas that emerged
217
+ 4. Recommended next steps or actions
218
+ """
219
+
220
+ resp = get_llm_response(synthesis_prompt, model=model, provider=provider, npc=npc, temperature=0.4)
221
+ synthesis = str(resp.get('response', ''))
222
+ print(synthesis)
223
+
224
+ context['output'] = synthesis
225
+ context['messages'] = messages
226
+ context['convene_result'] = {
227
+ 'topic': topic,
228
+ 'participants': npc_names,
229
+ 'rounds': rounds,
230
+ 'discussion': discussion_log,
231
+ 'synthesis': synthesis
232
+ }
@@ -0,0 +1,184 @@
1
+ jinx_name: delegate
2
+ description: Delegate a task to another NPC with review and feedback loop until completion. Choose the NPC whose directive best matches the task.
3
+ inputs:
4
+ - npc_name:
5
+ description: "Name of the NPC to delegate to"
6
+ - task:
7
+ description: "The task or request to delegate to the NPC"
8
+ - max_iterations: "10"
9
+ steps:
10
+ - name: delegate_with_review
11
+ engine: python
12
+ code: |
13
+ from termcolor import colored
14
+ from npcpy.llm_funcs import get_llm_response
15
+
16
+ # Try to get spinner for status updates
17
+ try:
18
+ from npcsh.ui import get_current_spinner
19
+ spinner = get_current_spinner()
20
+ except:
21
+ spinner = None
22
+
23
+ target_name = {{ npc_name | default("") | tojson }}.lower().strip()
24
+ task_request = {{ task | default("") | tojson }}
25
+ max_iters = int({{ max_iterations | default("10") | tojson }} or "10")
26
+
27
+ team_obj = context.get('team') or getattr(npc, 'team', None)
28
+ orchestrator = context.get('npc') or npc
29
+ orchestrator_name = getattr(orchestrator, 'name', 'orchestrator')
30
+
31
+ if not team_obj:
32
+ output = "Error: No team available for delegation"
33
+ exit()
34
+
35
+ if not hasattr(team_obj, 'npcs') or target_name not in team_obj.npcs:
36
+ available = list(team_obj.npcs.keys()) if hasattr(team_obj, 'npcs') else []
37
+ output = "Error: NPC '{}' not found. Available: {}".format(target_name, ', '.join(available))
38
+ exit()
39
+
40
+ target_npc = team_obj.npcs[target_name]
41
+ target_jinxs = dict((k, v) for k, v in target_npc.jinxs_dict.items() if k != 'delegate')
42
+
43
+ sep = '-' * 60
44
+ print(colored("\n" + sep, "cyan"))
45
+ print(colored(" Delegating to @" + target_name, "yellow", attrs=["bold"]))
46
+ task_preview = task_request[:100] + ('...' if len(task_request) > 100 else '')
47
+ print(colored(" Task: " + task_preview, "white", attrs=["dark"]))
48
+ print(colored(sep + "\n", "cyan"))
49
+ print(colored(" [{}] Model: {}".format(target_name, target_npc.model), "white", attrs=["dark"]))
50
+ jinx_list = ', '.join(list(target_jinxs.keys())[:8])
51
+ print(colored(" [{}] Jinxs: {}...".format(target_name, jinx_list), "white", attrs=["dark"]))
52
+
53
+ # Update spinner to show sub-agent
54
+ if spinner:
55
+ spinner.set_message("{} delegated to {}".format(orchestrator_name, target_name))
56
+
57
+ current_task = task_request
58
+ iteration = 0
59
+ final_output = None
60
+ task_complete = False
61
+
62
+ while iteration < max_iters and not task_complete:
63
+ iteration += 1
64
+
65
+ # Update spinner with current iteration
66
+ if spinner:
67
+ spinner.set_message("{} working (iter {}/{})".format(target_name, iteration, max_iters))
68
+
69
+ if iteration > 1:
70
+ print(colored("\n" + sep, "yellow"))
71
+ iter_msg = " Iteration {}/{} - Re-tasking @{}".format(iteration, max_iters, target_name)
72
+ print(colored(iter_msg, "yellow", attrs=["bold"]))
73
+ print(colored(sep + "\n", "yellow"))
74
+
75
+ try:
76
+ result = target_npc.check_llm_command(
77
+ current_task,
78
+ context=context,
79
+ team=team_obj,
80
+ jinxs=target_jinxs,
81
+ stream=False,
82
+ )
83
+
84
+ if isinstance(result, dict):
85
+ delegate_output = result.get('output') or result.get('response') or str(result)
86
+ delegate_messages = result.get('messages', [])
87
+ else:
88
+ delegate_output = str(result)
89
+ delegate_messages = []
90
+
91
+ print(colored("\n" + sep, "cyan"))
92
+ print(colored(" @{} iteration {} complete".format(target_name, iteration), "green"))
93
+ print(colored(sep + "\n", "cyan"))
94
+
95
+ # Update spinner for review phase
96
+ if spinner:
97
+ spinner.set_message("{} reviewing {}'s work".format(orchestrator_name, target_name))
98
+
99
+ # Build review prompt without f-strings to avoid YAML issues
100
+ output_preview = delegate_output[:2000] if delegate_output else 'No output received'
101
+ msg_preview = str(delegate_messages[-5:]) if delegate_messages else 'No messages'
102
+
103
+ review_lines = [
104
+ "You are reviewing work done by @{} on this task:".format(target_name),
105
+ "",
106
+ "ORIGINAL TASK: " + task_request,
107
+ "",
108
+ "ITERATION: {}/{}".format(iteration, max_iters),
109
+ "",
110
+ "SUB-AGENT OUTPUT:",
111
+ output_preview,
112
+ "",
113
+ "RECENT MESSAGES:",
114
+ msg_preview,
115
+ "",
116
+ "Evaluate if the task is complete. Consider:",
117
+ "1. Did the sub-agent accomplish what was asked?",
118
+ "2. Are there obvious errors or incomplete steps?",
119
+ "3. For GUI tasks: Did they fill in all required fields?",
120
+ "",
121
+ "Respond EXACTLY like this:",
122
+ "COMPLETE: YES or NO",
123
+ "FEEDBACK: If NO, what should be done next",
124
+ "SUMMARY: Brief summary of progress"
125
+ ]
126
+ review_prompt = "\n".join(review_lines)
127
+
128
+ review_result = get_llm_response(
129
+ review_prompt,
130
+ model=getattr(orchestrator, 'model', 'gemini-2.5-flash'),
131
+ provider=getattr(orchestrator, 'provider', 'gemini'),
132
+ npc=orchestrator,
133
+ temperature=0.3
134
+ )
135
+
136
+ review_text = str(review_result.get('response', ''))
137
+ is_complete = 'COMPLETE: YES' in review_text.upper()
138
+
139
+ feedback = ""
140
+ if 'FEEDBACK:' in review_text:
141
+ fb_start = review_text.find('FEEDBACK:') + 9
142
+ fb_end = review_text.find('SUMMARY:', fb_start) if 'SUMMARY:' in review_text else len(review_text)
143
+ feedback = review_text[fb_start:fb_end].strip()
144
+
145
+ summary = ""
146
+ if 'SUMMARY:' in review_text:
147
+ summary = review_text[review_text.find('SUMMARY:') + 8:].strip()
148
+
149
+ if is_complete:
150
+ task_complete = True
151
+ print(colored("\n Task completed successfully", "green", attrs=["bold"]))
152
+ if summary:
153
+ print(colored(" Summary: " + summary[:200], "white", attrs=["dark"]))
154
+ final_output = "[{}] Task completed.\n{}".format(target_name, summary)
155
+ else:
156
+ print(colored("\n Task incomplete - providing feedback", "yellow"))
157
+ if feedback:
158
+ print(colored(" Feedback: " + feedback[:200] + "...", "white", attrs=["dark"]))
159
+
160
+ followup_lines = [
161
+ "Continue the previous task. Feedback from orchestrator:",
162
+ "",
163
+ "ORIGINAL TASK: " + task_request,
164
+ "",
165
+ "FEEDBACK: " + feedback,
166
+ "",
167
+ "Continue and complete the task based on this feedback."
168
+ ]
169
+ current_task = "\n".join(followup_lines)
170
+
171
+ if delegate_messages:
172
+ context['messages'] = delegate_messages
173
+
174
+ except Exception as e:
175
+ print(colored(" Error in iteration {}: {}".format(iteration, e), "red"))
176
+ final_output = "Error delegating to {}: {}".format(target_name, str(e))
177
+ break
178
+
179
+ if not task_complete and iteration >= max_iters:
180
+ print(colored("\n Max iterations ({}) reached".format(max_iters), "yellow"))
181
+ status = summary if summary else 'Unknown'
182
+ final_output = "[{}] Task incomplete after {} iterations. Status: {}".format(target_name, max_iters, status)
183
+
184
+ output = final_output or "[{}]: No output received".format(target_name)