npcsh 1.1.12__py3-none-any.whl → 1.1.14__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.
- npcsh/_state.py +700 -377
- npcsh/alicanto.py +54 -1153
- npcsh/completion.py +206 -0
- npcsh/config.py +163 -0
- npcsh/corca.py +35 -1462
- npcsh/execution.py +185 -0
- npcsh/guac.py +31 -1986
- npcsh/npc_team/jinxs/code/sh.jinx +11 -15
- npcsh/npc_team/jinxs/modes/alicanto.jinx +186 -80
- npcsh/npc_team/jinxs/modes/corca.jinx +243 -22
- npcsh/npc_team/jinxs/modes/guac.jinx +313 -42
- npcsh/npc_team/jinxs/modes/plonk.jinx +209 -48
- npcsh/npc_team/jinxs/modes/pti.jinx +167 -25
- npcsh/npc_team/jinxs/modes/spool.jinx +158 -37
- npcsh/npc_team/jinxs/modes/wander.jinx +179 -74
- npcsh/npc_team/jinxs/modes/yap.jinx +258 -21
- npcsh/npc_team/jinxs/utils/chat.jinx +39 -12
- npcsh/npc_team/jinxs/utils/cmd.jinx +44 -0
- npcsh/npc_team/jinxs/utils/search.jinx +3 -3
- npcsh/npc_team/jinxs/utils/usage.jinx +33 -0
- npcsh/npcsh.py +76 -20
- npcsh/parsing.py +118 -0
- npcsh/plonk.py +41 -329
- npcsh/pti.py +41 -201
- npcsh/spool.py +34 -239
- npcsh/ui.py +199 -0
- npcsh/wander.py +54 -542
- npcsh/yap.py +38 -570
- npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.jinx +194 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/chat.jinx +44 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/cmd.jinx +44 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/corca.jinx +249 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/guac.jinx +317 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/plonk.jinx +214 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/pti.jinx +170 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/search.jinx +3 -3
- npcsh-1.1.14.data/data/npcsh/npc_team/sh.jinx +34 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/spool.jinx +161 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/usage.jinx +33 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/wander.jinx +186 -0
- npcsh-1.1.14.data/data/npcsh/npc_team/yap.jinx +262 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/METADATA +1 -1
- npcsh-1.1.14.dist-info/RECORD +135 -0
- npcsh-1.1.12.data/data/npcsh/npc_team/alicanto.jinx +0 -88
- npcsh-1.1.12.data/data/npcsh/npc_team/chat.jinx +0 -17
- npcsh-1.1.12.data/data/npcsh/npc_team/corca.jinx +0 -28
- npcsh-1.1.12.data/data/npcsh/npc_team/guac.jinx +0 -46
- npcsh-1.1.12.data/data/npcsh/npc_team/plonk.jinx +0 -53
- npcsh-1.1.12.data/data/npcsh/npc_team/pti.jinx +0 -28
- npcsh-1.1.12.data/data/npcsh/npc_team/sh.jinx +0 -38
- npcsh-1.1.12.data/data/npcsh/npc_team/spool.jinx +0 -40
- npcsh-1.1.12.data/data/npcsh/npc_team/wander.jinx +0 -81
- npcsh-1.1.12.data/data/npcsh/npc_team/yap.jinx +0 -25
- npcsh-1.1.12.dist-info/RECORD +0 -126
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/agent.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/foreman.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/WHEEL +0 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/entry_points.txt +0 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/top_level.txt +0 -0
npcsh/parsing.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command parsing utilities for npcsh
|
|
3
|
+
"""
|
|
4
|
+
import shlex
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def split_by_pipes(command: str) -> List[str]:
|
|
9
|
+
"""
|
|
10
|
+
Split a command by pipes, preserving quoted strings.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
'foo | bar' -> ['foo', 'bar']
|
|
14
|
+
'foo "hello|world" | bar' -> ['foo "hello|world"', 'bar']
|
|
15
|
+
"""
|
|
16
|
+
result = []
|
|
17
|
+
current = []
|
|
18
|
+
in_single_quote = False
|
|
19
|
+
in_double_quote = False
|
|
20
|
+
i = 0
|
|
21
|
+
|
|
22
|
+
while i < len(command):
|
|
23
|
+
char = command[i]
|
|
24
|
+
|
|
25
|
+
if char == "'" and not in_double_quote:
|
|
26
|
+
in_single_quote = not in_single_quote
|
|
27
|
+
current.append(char)
|
|
28
|
+
elif char == '"' and not in_single_quote:
|
|
29
|
+
in_double_quote = not in_double_quote
|
|
30
|
+
current.append(char)
|
|
31
|
+
elif char == '|' and not in_single_quote and not in_double_quote:
|
|
32
|
+
result.append(''.join(current).strip())
|
|
33
|
+
current = []
|
|
34
|
+
else:
|
|
35
|
+
current.append(char)
|
|
36
|
+
|
|
37
|
+
i += 1
|
|
38
|
+
|
|
39
|
+
# Add final segment
|
|
40
|
+
if current:
|
|
41
|
+
result.append(''.join(current).strip())
|
|
42
|
+
|
|
43
|
+
return [s for s in result if s]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_command_safely(cmd: str) -> List[str]:
|
|
47
|
+
"""
|
|
48
|
+
Safely parse a command string into parts using shlex.
|
|
49
|
+
|
|
50
|
+
Returns an empty list on parse errors.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
return shlex.split(cmd)
|
|
54
|
+
except ValueError:
|
|
55
|
+
# Handle unmatched quotes, etc
|
|
56
|
+
return cmd.split()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_generic_command_flags(parts: List[str]) -> tuple:
|
|
60
|
+
"""
|
|
61
|
+
Parse command flags in a generic way.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Tuple of (parsed_flags dict, remaining_parts list)
|
|
65
|
+
"""
|
|
66
|
+
parsed_flags = {}
|
|
67
|
+
remaining = []
|
|
68
|
+
|
|
69
|
+
i = 0
|
|
70
|
+
while i < len(parts):
|
|
71
|
+
part = parts[i]
|
|
72
|
+
|
|
73
|
+
if part.startswith('--'):
|
|
74
|
+
key = part[2:]
|
|
75
|
+
if '=' in key:
|
|
76
|
+
key, value = key.split('=', 1)
|
|
77
|
+
parsed_flags[key] = _try_convert_type(value)
|
|
78
|
+
elif i + 1 < len(parts) and not parts[i + 1].startswith('-'):
|
|
79
|
+
parsed_flags[key] = _try_convert_type(parts[i + 1])
|
|
80
|
+
i += 1
|
|
81
|
+
else:
|
|
82
|
+
parsed_flags[key] = True
|
|
83
|
+
elif part.startswith('-') and len(part) == 2:
|
|
84
|
+
key = part[1]
|
|
85
|
+
if i + 1 < len(parts) and not parts[i + 1].startswith('-'):
|
|
86
|
+
parsed_flags[key] = _try_convert_type(parts[i + 1])
|
|
87
|
+
i += 1
|
|
88
|
+
else:
|
|
89
|
+
parsed_flags[key] = True
|
|
90
|
+
else:
|
|
91
|
+
remaining.append(part)
|
|
92
|
+
|
|
93
|
+
i += 1
|
|
94
|
+
|
|
95
|
+
return parsed_flags, remaining
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _try_convert_type(value: str):
|
|
99
|
+
"""Try to convert string value to appropriate Python type"""
|
|
100
|
+
# Try int
|
|
101
|
+
try:
|
|
102
|
+
return int(value)
|
|
103
|
+
except ValueError:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
# Try float
|
|
107
|
+
try:
|
|
108
|
+
return float(value)
|
|
109
|
+
except ValueError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# Try bool
|
|
113
|
+
if value.lower() in ('true', 'yes', '1'):
|
|
114
|
+
return True
|
|
115
|
+
if value.lower() in ('false', 'no', '0'):
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
return value
|
npcsh/plonk.py
CHANGED
|
@@ -1,342 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import platform
|
|
5
|
-
from npcpy.llm_funcs import get_llm_response
|
|
6
|
-
from npcpy.work.desktop import perform_action
|
|
7
|
-
import matplotlib.pyplot as plt
|
|
8
|
-
import matplotlib.patches as patches
|
|
9
|
-
from PIL import Image
|
|
10
|
-
import numpy as np
|
|
11
|
-
import imagehash
|
|
12
|
-
from npcsh._state import NPCSH_VISION_MODEL, NPCSH_VISION_PROVIDER
|
|
13
|
-
import argparse
|
|
14
|
-
from npcpy.npc_compiler import NPC
|
|
15
|
-
|
|
16
|
-
def get_system_examples():
|
|
17
|
-
system = platform.system()
|
|
18
|
-
if system == "Windows":
|
|
19
|
-
return "Examples: start firefox, notepad, calc, explorer"
|
|
20
|
-
elif system == "Darwin":
|
|
21
|
-
return "Examples: open -a Firefox, open -a TextEdit, open -a Calculator"
|
|
22
|
-
else:
|
|
23
|
-
return "Examples: firefox &, gedit &, gnome-calculator &"
|
|
24
|
-
|
|
25
|
-
def format_plonk_summary(synthesized_summary: list) -> str:
|
|
26
|
-
"""Formats the summary of a plonk session into a readable markdown report."""
|
|
27
|
-
if not synthesized_summary:
|
|
28
|
-
return "Plonk session ended with no actions performed."
|
|
29
|
-
|
|
30
|
-
output = "## Plonk Session Summary\n\n"
|
|
31
|
-
for info in synthesized_summary:
|
|
32
|
-
iteration = info.get('iteration', 'N/A')
|
|
33
|
-
feedback = info.get('last_action_feedback', 'None')
|
|
34
|
-
coords = info.get('last_click_coords', 'None')
|
|
35
|
-
output += f"### Iteration {iteration}\n"
|
|
36
|
-
output += f"- **Feedback:** {feedback}\n"
|
|
37
|
-
output += f"- **Last Click:** {coords}\n\n"
|
|
38
|
-
return output
|
|
39
|
-
|
|
40
|
-
def get_image_hash(image_path):
|
|
41
|
-
"""Generate a perceptual hash of the image to detect screen changes intelligently."""
|
|
42
|
-
try:
|
|
43
|
-
|
|
44
|
-
return imagehash.phash(Image.open(image_path))
|
|
45
|
-
except Exception as e:
|
|
46
|
-
print(f"Could not generate image hash: {e}")
|
|
47
|
-
return None
|
|
48
|
-
|
|
49
|
-
def add_click_vector_trail(image_path, click_history, output_path):
|
|
50
|
-
"""Add click markers showing the progression/trail of clicks with arrows and numbers."""
|
|
51
|
-
try:
|
|
52
|
-
img = Image.open(image_path)
|
|
53
|
-
img_array = np.array(img)
|
|
54
|
-
height, width = img_array.shape[:2]
|
|
55
|
-
|
|
56
|
-
fig, ax = plt.subplots(1, 1, figsize=(width/100, height/100), dpi=100)
|
|
57
|
-
ax.imshow(img_array)
|
|
58
|
-
|
|
59
|
-
font_size = max(12, min(width, height) // 80)
|
|
60
|
-
colors = plt.cm.viridis(np.linspace(0.3, 1.0, len(click_history)))
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if len(click_history) > 1:
|
|
64
|
-
for i in range(len(click_history) - 1):
|
|
65
|
-
x1, y1 = (click_history[i]['x'] * width / 100, click_history[i]['y'] * height / 100)
|
|
66
|
-
x2, y2 = (click_history[i+1]['x'] * width / 100, click_history[i+1]['y'] * height / 100)
|
|
67
|
-
ax.annotate('', xy=(x2, y2), xytext=(x1, y1),
|
|
68
|
-
arrowprops=dict(arrowstyle='->,head_width=0.6,head_length=0.8',
|
|
69
|
-
lw=3, color='cyan', alpha=0.9, shrinkA=25, shrinkB=25))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
for i, click in enumerate(click_history):
|
|
73
|
-
x_pixel = int(click['x'] * width / 100)
|
|
74
|
-
y_pixel = int(click['y'] * height / 100)
|
|
75
|
-
|
|
76
|
-
radius = 25
|
|
77
|
-
circle = patches.Circle((x_pixel, y_pixel), radius=radius,
|
|
78
|
-
linewidth=3, edgecolor='white',
|
|
79
|
-
facecolor=colors[i], alpha=0.9)
|
|
80
|
-
ax.add_patch(circle)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
ax.text(x_pixel, y_pixel, str(i+1),
|
|
84
|
-
fontsize=font_size + 4,
|
|
85
|
-
color='white', weight='bold', ha='center', va='center')
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
coord_text = f"({click['x']}, {click['y']})"
|
|
89
|
-
ax.text(x_pixel + radius + 5,
|
|
90
|
-
y_pixel,
|
|
91
|
-
coord_text,
|
|
92
|
-
fontsize=font_size,
|
|
93
|
-
color='white',
|
|
94
|
-
weight='bold',
|
|
95
|
-
ha='left', va='center',
|
|
96
|
-
bbox=dict(boxstyle="round,pad=0.2", facecolor=colors[i],
|
|
97
|
-
alpha=0.9, edgecolor='white'))
|
|
98
|
-
|
|
99
|
-
ax.set_xlim(0, width)
|
|
100
|
-
ax.set_ylim(height, 0)
|
|
101
|
-
ax.axis('off')
|
|
102
|
-
plt.tight_layout(pad=0)
|
|
103
|
-
plt.savefig(output_path, bbox_inches='tight', pad_inches=0, dpi=100)
|
|
104
|
-
plt.close()
|
|
105
|
-
|
|
106
|
-
return True
|
|
107
|
-
except Exception as e:
|
|
108
|
-
print(f"Failed to add click trail with matplotlib: {e}")
|
|
109
|
-
return False
|
|
110
|
-
|
|
111
|
-
def execute_plonk_command(request, model, provider, npc=None, plonk_context=None, max_iterations=10, debug=False):
|
|
112
|
-
system_examples = get_system_examples()
|
|
113
|
-
messages = []
|
|
114
|
-
last_action_feedback = "None"
|
|
115
|
-
last_click_coords = None
|
|
116
|
-
synthesized_summary = []
|
|
117
|
-
|
|
118
|
-
current_screen_hash = None
|
|
119
|
-
click_history = []
|
|
120
|
-
HASH_DISTANCE_THRESHOLD = 3
|
|
121
|
-
|
|
122
|
-
for iteration_count in range(max_iterations):
|
|
123
|
-
try:
|
|
124
|
-
screenshot_info = capture_screenshot(full=True)
|
|
125
|
-
screenshot_path = screenshot_info.get('file_path') if screenshot_info else None
|
|
126
|
-
|
|
127
|
-
if not screenshot_path:
|
|
128
|
-
last_action_feedback = "Error: Failed to capture screenshot."
|
|
129
|
-
time.sleep(1)
|
|
130
|
-
continue
|
|
131
|
-
|
|
132
|
-
new_screen_hash = get_image_hash(screenshot_path)
|
|
133
|
-
|
|
134
|
-
if current_screen_hash is None or (new_screen_hash - current_screen_hash > HASH_DISTANCE_THRESHOLD):
|
|
135
|
-
if debug and current_screen_hash is not None:
|
|
136
|
-
print(f"Screen changed (hash distance: {new_screen_hash - current_screen_hash}) - resetting click history.")
|
|
137
|
-
click_history = []
|
|
138
|
-
current_screen_hash = new_screen_hash
|
|
139
|
-
|
|
140
|
-
summary_info = {
|
|
141
|
-
'iteration': iteration_count + 1,
|
|
142
|
-
'last_action_feedback': last_action_feedback,
|
|
143
|
-
'last_click_coords': click_history[-1] if click_history else None
|
|
144
|
-
}
|
|
145
|
-
synthesized_summary.append(summary_info)
|
|
146
|
-
|
|
147
|
-
if debug:
|
|
148
|
-
print(f"Iteration {iteration_count + 1}/{max_iterations}")
|
|
149
|
-
|
|
150
|
-
context_injection = ""
|
|
151
|
-
if plonk_context:
|
|
152
|
-
context_injection = f"""
|
|
153
|
-
---
|
|
154
|
-
IMPORTANT TEAM CONTEXT FOR THIS TASK:
|
|
155
|
-
{plonk_context}
|
|
156
|
-
---
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
completion_example_text = """
|
|
160
|
-
{
|
|
161
|
-
"actions": [],
|
|
162
|
-
"status": "Task appears complete. Waiting for user approval to proceed or finish."
|
|
163
|
-
}
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
quit_rule_text = 'NEVER include {"type": "quit"} in your actions - the user controls when to stop.'
|
|
167
|
-
|
|
168
|
-
prompt_examples = """
|
|
169
|
-
---
|
|
170
|
-
EXAMPLE 1: Task "Create and save a file named 'memo.txt' with the text 'Meeting at 3pm'"
|
|
171
|
-
{
|
|
172
|
-
"actions": [
|
|
173
|
-
{ "type": "bash", "command": "gedit &" },
|
|
1
|
+
"""
|
|
2
|
+
plonk - Vision-based GUI automation CLI entry point
|
|
174
3
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{
|
|
181
|
-
"actions": [
|
|
182
|
-
{ "type": "bash", "command": "firefox &" },
|
|
183
|
-
|
|
184
|
-
]
|
|
185
|
-
}
|
|
186
|
-
---
|
|
187
|
-
EXAMPLE 3: Task "Click the red button on the form"
|
|
188
|
-
{
|
|
189
|
-
"actions": [
|
|
190
|
-
{ "type": "click", "x": 75, "y": 45 }
|
|
191
|
-
]
|
|
192
|
-
}
|
|
193
|
-
---
|
|
194
|
-
EXAMPLE 4: Task "Open Gmail and draft a reply to most recent email"
|
|
195
|
-
{
|
|
196
|
-
"actions": [
|
|
197
|
-
{ "type": "bash", "command": "open -a Safari" },
|
|
198
|
-
|
|
199
|
-
]
|
|
200
|
-
}
|
|
201
|
-
"""
|
|
202
|
-
|
|
203
|
-
prompt_template = f"""
|
|
204
|
-
Goal: {request}
|
|
205
|
-
Feedback from last action: {last_action_feedback}
|
|
206
|
-
|
|
207
|
-
{context_injection}
|
|
208
|
-
|
|
209
|
-
Your task is to control the computer to achieve the goal.
|
|
210
|
-
|
|
211
|
-
IMPORTANT: You should take actions step-by-step and verify each step works before proceeding.
|
|
212
|
-
DO NOT plan all actions at once - take a few actions, then look at the screen again.
|
|
213
|
-
|
|
214
|
-
CRITICAL: NEVER use the 'quit' action automatically. Even if the task appears complete,
|
|
215
|
-
continue working or wait for user guidance. The user will decide when to quit.
|
|
216
|
-
|
|
217
|
-
THOUGHT PROCESS:
|
|
218
|
-
1. Analyze the screen. Is the application I need (e.g., a web browser) already open?
|
|
219
|
-
2. If YES, `click` it. If NO, use `bash` to launch it. Use the examples: {system_examples}.
|
|
220
|
-
3. Take 2-3 actions maximum, then let me see the screen again to verify progress.
|
|
221
|
-
4. If task appears complete, explain status but DO NOT quit - wait for user direction.
|
|
222
|
-
|
|
223
|
-
Your response MUST be a JSON object with an "actions" key.
|
|
224
|
-
All clicking actions should use percentage coordinates relative
|
|
225
|
-
to the screen size.
|
|
226
|
-
The x and y are (0,0) at the TOP LEFT CORNER OF THE SCREEN.
|
|
227
|
-
|
|
228
|
-
MAXIMUM 3 ACTIONS PER RESPONSE - then let me see the screen to verify progress.
|
|
229
|
-
Never do more than one click, type, or hotkey event per response. It is important to take a sequence of
|
|
230
|
-
slow actions separated to avoid making mistakes and falling in loops.
|
|
231
|
-
|
|
232
|
-
If the task appears complete, you can include an empty actions list and explain:
|
|
233
|
-
{completion_example_text}
|
|
234
|
-
|
|
235
|
-
{quit_rule_text}
|
|
236
|
-
""" + prompt_examples
|
|
4
|
+
This is a thin wrapper that executes the plonk.jinx through the jinx mechanism.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
237
9
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if click_history:
|
|
241
|
-
marked_image_path = "/tmp/marked_screenshot.png"
|
|
242
|
-
if add_click_vector_trail(screenshot_path, click_history, marked_image_path):
|
|
243
|
-
image_to_send_path = marked_image_path
|
|
244
|
-
if debug:
|
|
245
|
-
print(f"Drew click trail with {len(click_history)} points.")
|
|
10
|
+
from npcsh._state import setup_shell
|
|
246
11
|
|
|
247
|
-
response = get_llm_response(prompt_template, model=model, provider=provider, npc=npc,
|
|
248
|
-
images=[image_to_send_path], messages=messages, format="json")
|
|
249
|
-
messages = response.get("messages", messages)
|
|
250
|
-
response_data = response.get('response')
|
|
251
|
-
|
|
252
|
-
if debug:
|
|
253
|
-
print(response_data)
|
|
254
12
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description="plonk - Vision-based GUI automation")
|
|
15
|
+
parser.add_argument("task", nargs="*", help="Task description for GUI automation")
|
|
16
|
+
parser.add_argument("--vmodel", type=str, help="Vision model to use (default: gpt-4o)")
|
|
17
|
+
parser.add_argument("--vprovider", type=str, help="Vision provider (default: openai)")
|
|
18
|
+
parser.add_argument("--max-iterations", type=int, default=10, help="Maximum iterations")
|
|
19
|
+
parser.add_argument("--no-debug", action="store_true", help="Disable debug output")
|
|
20
|
+
args = parser.parse_args()
|
|
258
21
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
continue
|
|
263
|
-
|
|
264
|
-
for action in actions_list:
|
|
265
|
-
if debug:
|
|
266
|
-
print(f"Executing action: {action}")
|
|
267
|
-
|
|
268
|
-
if action.get("type") == "quit":
|
|
269
|
-
print("⚠️ Model attempted to quit automatically. Ignoring.")
|
|
270
|
-
continue
|
|
271
|
-
|
|
272
|
-
result = perform_action(action)
|
|
273
|
-
last_action_feedback = result.get("message") or result.get("output")
|
|
22
|
+
if not args.task:
|
|
23
|
+
parser.print_help()
|
|
24
|
+
sys.exit(1)
|
|
274
25
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
click_history.append(click_info)
|
|
278
|
-
if len(click_history) > 6:
|
|
279
|
-
click_history.pop(0)
|
|
280
|
-
|
|
281
|
-
if result.get("status") == "error":
|
|
282
|
-
last_action_feedback = f"Action failed: {last_action_feedback}"
|
|
283
|
-
print(f"Action failed, providing feedback to model: {last_action_feedback}")
|
|
284
|
-
break
|
|
285
|
-
time.sleep(1)
|
|
26
|
+
# Setup shell to get team and default NPC
|
|
27
|
+
command_history, team, default_npc = setup_shell()
|
|
286
28
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
29
|
+
if not team or "plonk" not in team.jinxs_dict:
|
|
30
|
+
print("Error: plonk jinx not found. Ensure npc_team/jinxs/modes/plonk.jinx exists.")
|
|
31
|
+
sys.exit(1)
|
|
290
32
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
last_action_feedback = "User provided new guidance to correct the course."
|
|
303
|
-
print("✅ Guidance received. Resuming with updated instructions...")
|
|
304
|
-
else:
|
|
305
|
-
last_action_feedback = "User paused and resumed without new guidance."
|
|
306
|
-
print("✅ No guidance provided. Resuming...")
|
|
307
|
-
continue
|
|
308
|
-
except EOFError:
|
|
309
|
-
print("\nExiting plonk mode.")
|
|
310
|
-
break
|
|
33
|
+
# Build context for jinx execution
|
|
34
|
+
context = {
|
|
35
|
+
"npc": default_npc,
|
|
36
|
+
"team": team,
|
|
37
|
+
"messages": [],
|
|
38
|
+
"task": " ".join(args.task),
|
|
39
|
+
"vmodel": args.vmodel,
|
|
40
|
+
"vprovider": args.vprovider,
|
|
41
|
+
"max_iterations": args.max_iterations,
|
|
42
|
+
"debug": not args.no_debug,
|
|
43
|
+
}
|
|
311
44
|
|
|
312
|
-
|
|
45
|
+
# Execute the jinx
|
|
46
|
+
plonk_jinx = team.jinxs_dict["plonk"]
|
|
47
|
+
result = plonk_jinx.execute(context=context, npc=default_npc)
|
|
313
48
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
parser.add_argument("request", help="The task to perform")
|
|
317
|
-
parser.add_argument("--model", help="Model to use")
|
|
318
|
-
parser.add_argument("--provider", help="Provider to use")
|
|
319
|
-
parser.add_argument("--max-iterations", type=int, default=10, help="Maximum iterations")
|
|
320
|
-
parser.add_argument("--debug", action="store_true", help="Enable debug output")
|
|
321
|
-
parser.add_argument("--npc", type=str, default=os.path.expanduser('~/.npcsh/npc_team/plonk.npc'), help="Path to NPC file")
|
|
322
|
-
|
|
323
|
-
args = parser.parse_args()
|
|
324
|
-
|
|
325
|
-
npc = NPC(file=args.npc) if os.path.exists(os.path.expanduser(args.npc)) else None
|
|
326
|
-
|
|
327
|
-
model = args.model or (npc.model if npc else NPCSH_VISION_MODEL)
|
|
328
|
-
provider = args.provider or (npc.provider if npc else NPCSH_VISION_PROVIDER)
|
|
49
|
+
if isinstance(result, dict) and result.get("output"):
|
|
50
|
+
print(result["output"])
|
|
329
51
|
|
|
330
|
-
summary = execute_plonk_command(
|
|
331
|
-
request=args.request,
|
|
332
|
-
model=model,
|
|
333
|
-
provider=provider,
|
|
334
|
-
npc=npc,
|
|
335
|
-
max_iterations=args.max_iterations,
|
|
336
|
-
debug=args.debug
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
print(format_plonk_summary(summary))
|
|
340
52
|
|
|
341
53
|
if __name__ == "__main__":
|
|
342
|
-
main()
|
|
54
|
+
main()
|