@ycheung5/pi-computer-use 0.0.0-stage → 0.1.1
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.
- package/GLOSSARY.md +38 -0
- package/LICENSE +21 -0
- package/README.md +165 -2
- package/assets/accessibility.cs +208 -0
- package/assets/browser-indicators.cs +37 -0
- package/assets/capture.cs +215 -0
- package/assets/clipboard.cs +197 -0
- package/assets/controls.cs +249 -0
- package/assets/indicator-preview.png +0 -0
- package/assets/indicators.cs +252 -0
- package/assets/input-timing.cs +42 -0
- package/assets/node-LICENSE.txt +2732 -0
- package/assets/ocr.cs +86 -0
- package/assets/ownership.cs +59 -0
- package/assets/pointer.cs +241 -0
- package/assets/puppeteer-LICENSE.txt +202 -0
- package/assets/readme-banner.svg +33 -0
- package/assets/stop.cs +197 -0
- package/assets/text.cs +279 -0
- package/assets/wgc.cs +167 -0
- package/assets/window-management.cs +217 -0
- package/assets/window-marker.cs +23 -0
- package/assets/window-references.cs +80 -0
- package/assets/windows.cs +127 -0
- package/dist/accessibility.d.ts +29 -0
- package/dist/accessibility.js +258 -0
- package/dist/browser-actions.d.ts +22 -0
- package/dist/browser-actions.js +246 -0
- package/dist/browser-indicators.d.ts +23 -0
- package/dist/browser-indicators.js +69 -0
- package/dist/browser-profiles.d.ts +10 -0
- package/dist/browser-profiles.js +174 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +2071 -0
- package/dist/capture.d.ts +74 -0
- package/dist/capture.js +199 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +384 -0
- package/dist/desktop.d.ts +20 -0
- package/dist/desktop.js +42 -0
- package/dist/extension.d.ts +4 -0
- package/dist/extension.js +409 -0
- package/dist/input-timing.d.ts +24 -0
- package/dist/input-timing.js +70 -0
- package/dist/launcher.d.ts +6 -0
- package/dist/launcher.js +85 -0
- package/dist/native.d.ts +11 -0
- package/dist/native.js +146 -0
- package/dist/ocr.d.ts +52 -0
- package/dist/ocr.js +246 -0
- package/dist/ownership.d.ts +4 -0
- package/dist/ownership.js +24 -0
- package/dist/pointer.d.ts +128 -0
- package/dist/pointer.js +198 -0
- package/dist/protocol.d.ts +15 -0
- package/dist/protocol.js +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +72 -0
- package/dist/settings.d.ts +85 -0
- package/dist/settings.js +449 -0
- package/dist/stop.d.ts +24 -0
- package/dist/stop.js +156 -0
- package/dist/text.d.ts +120 -0
- package/dist/text.js +212 -0
- package/dist/waits.d.ts +19 -0
- package/dist/waits.js +262 -0
- package/dist/window-management.d.ts +25 -0
- package/dist/window-management.js +167 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +553 -0
- package/docs/README.md +55 -0
- package/docs/adr/0001-preserve-reference-behavior.md +9 -0
- package/docs/adr/0002-window-lifetime-references.md +11 -0
- package/docs/adr/0003-exclusive-desktop-control.md +11 -0
- package/docs/adr/0004-pi-first-acceptance.md +9 -0
- package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
- package/docs/agents/domain.md +9 -0
- package/docs/agents/issue-tracker.md +10 -0
- package/docs/agents/triage-labels.md +9 -0
- package/docs/browser.md +113 -0
- package/docs/implementation-plan.md +153 -0
- package/docs/indicators.md +53 -0
- package/docs/native-interop-research.md +17 -0
- package/docs/ownership.md +25 -0
- package/docs/pi-extension-research.md +57 -0
- package/docs/pointer.md +77 -0
- package/docs/runtime.md +42 -0
- package/docs/settings.md +33 -0
- package/docs/spec.md +230 -0
- package/docs/stopping.md +83 -0
- package/docs/text.md +99 -0
- package/docs/tickets.md +85 -0
- package/docs/validation.md +1420 -0
- package/docs/windows.md +171 -0
- package/package.json +134 -4
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Runtime.InteropServices;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Web.Script.Serialization;
|
|
6
|
+
|
|
7
|
+
// Focused Win32 bridge; guarded input lives in pointer.cs.
|
|
8
|
+
partial class WindowsBridge {
|
|
9
|
+
static bool physicalInputAnnounced;
|
|
10
|
+
static void NotifyPhysicalInput(bool active) {
|
|
11
|
+
if(physicalInputAnnounced==active)return;
|
|
12
|
+
physicalInputAnnounced=active;
|
|
13
|
+
Console.WriteLine(new JavaScriptSerializer().Serialize(new Dictionary<string,object> {{"event","input_state"},{"active",active},{"operationId",pointerOperation}}));
|
|
14
|
+
Console.Out.Flush();
|
|
15
|
+
}
|
|
16
|
+
delegate bool EnumWindow(IntPtr handle, IntPtr parameter);
|
|
17
|
+
[StructLayout(LayoutKind.Sequential)] struct Rect { public int Left, Top, Right, Bottom; }
|
|
18
|
+
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindow callback, IntPtr parameter);
|
|
19
|
+
[DllImport("user32.dll")] static extern bool IsWindow(IntPtr handle);
|
|
20
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr handle);
|
|
21
|
+
[DllImport("user32.dll")] static extern bool IsWindowEnabled(IntPtr handle);
|
|
22
|
+
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr handle);
|
|
23
|
+
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
|
|
24
|
+
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr handle, out Rect rect);
|
|
25
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr handle, StringBuilder text, int count);
|
|
26
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetClassName(IntPtr handle, StringBuilder text, int count);
|
|
27
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr handle, out uint process);
|
|
28
|
+
[DllImport("user32.dll", SetLastError=true)] static extern IntPtr SendMessageTimeout(IntPtr handle, uint message, UIntPtr wParam, IntPtr lParam, uint flags, uint timeout, out UIntPtr result);
|
|
29
|
+
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
|
|
30
|
+
[DllImport("user32.dll")] static extern bool SetProcessDpiAwarenessContext(IntPtr context);
|
|
31
|
+
|
|
32
|
+
static Dictionary<string, object> Result(bool ok, string action) {
|
|
33
|
+
return new Dictionary<string, object> { { "ok", ok }, { "action", action } };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static object ListWindows(Dictionary<string, object> args) {
|
|
37
|
+
if(args.ContainsKey("windowRef"))return ObserveReferencePresence(args);
|
|
38
|
+
int maximum = Convert.ToInt32(args["maxWindows"]);
|
|
39
|
+
bool invisible = Convert.ToBoolean(args["includeInvisible"]);
|
|
40
|
+
List<object> windows = new List<object>();
|
|
41
|
+
bool truncated=false,identityChanged=false,metadataOnly=Option(args,"_metadataOnly",false);
|
|
42
|
+
EnumWindows(delegate(IntPtr handle,IntPtr parameter) {
|
|
43
|
+
// Eligibility probes are not retained metadata. Re-read retained fields after binding.
|
|
44
|
+
if(!invisible && (!IsWindowVisible(handle) || IsIconic(handle)))return true;
|
|
45
|
+
uint pid;GetWindowThreadProcessId(handle,out pid);
|
|
46
|
+
if(pid==0 || !IsWindow(handle))return true;
|
|
47
|
+
if(windows.Count==maximum){truncated=true;return false;}
|
|
48
|
+
var bindingMetadata=new Dictionary<string,object>();
|
|
49
|
+
WindowReference binding=metadataOnly?null:ObserveWindowReference(handle,bindingMetadata);
|
|
50
|
+
bool visible=IsWindowVisible(handle),offscreen=!visible || IsIconic(handle);
|
|
51
|
+
if(!invisible && offscreen){if(binding!=null && !WindowMarker.Matches(handle,binding.Owner,binding.Marker))identityChanged=true;return true;}
|
|
52
|
+
Rect bounds;if(!GetWindowRect(handle,out bounds)){if(binding!=null)identityChanged=true;return true;}
|
|
53
|
+
var title=new StringBuilder(2048);var className=new StringBuilder(256);
|
|
54
|
+
GetWindowText(handle,title,title.Capacity);GetClassName(handle,className,className.Capacity);
|
|
55
|
+
UIntPtr ignored;bool answered=SendMessageTimeout(handle,0,UIntPtr.Zero,IntPtr.Zero,3,100,out ignored)!=IntPtr.Zero;
|
|
56
|
+
int responseError=answered?0:Marshal.GetLastWin32Error();
|
|
57
|
+
uint ownerAfter;GetWindowThreadProcessId(handle,out ownerAfter);
|
|
58
|
+
if(!IsWindow(handle) || ownerAfter!=pid || (binding!=null && !WindowMarker.Matches(handle,binding.Owner,binding.Marker))){if(binding!=null)identityChanged=true;return true;}
|
|
59
|
+
Dictionary<string,object> item=new Dictionary<string,object>{
|
|
60
|
+
{"depth",1},{"name",title.ToString()},{"className",className.ToString()},
|
|
61
|
+
{"controlType","Window"},{"processId",pid},{"nativeWindowHandle",handle.ToInt64()},
|
|
62
|
+
{"boundingBox",new Dictionary<string,object>{{"x",bounds.Left},{"y",bounds.Top},{"width",bounds.Right-bounds.Left},{"height",bounds.Bottom-bounds.Top},{"centerX",(bounds.Left+(double)bounds.Right)/2},{"centerY",(bounds.Top+(double)bounds.Bottom)/2}}},
|
|
63
|
+
{"isEnabled",IsWindowEnabled(handle)},{"isOffscreen",offscreen},{"hasKeyboardFocus",handle==GetForegroundWindow()},{"source","win32"},
|
|
64
|
+
{"responsive",responseError==5?(object)null:answered},{"responsiveness",answered?"responsive":responseError==5?"access_denied":"unresponsive_or_unknown"}
|
|
65
|
+
};
|
|
66
|
+
if(binding!=null){item["id"]="uia:hwnd:"+handle.ToInt64()+":pid:"+pid;item["windowRef"]=binding.Id;}
|
|
67
|
+
else if(metadataOnly)item["windowRefUnavailable"]="UNBOUND_METADATA: observe the selected window to obtain a reference";
|
|
68
|
+
else foreach(var pair in bindingMetadata)item[pair.Key]=pair.Value;
|
|
69
|
+
RememberPointerOrigin(handle);
|
|
70
|
+
if(binding!=null && !WindowMarker.Matches(handle,binding.Owner,binding.Marker)){identityChanged=true;return true;}
|
|
71
|
+
windows.Add(item);return true;
|
|
72
|
+
},IntPtr.Zero);
|
|
73
|
+
Dictionary<string, object> result = Result(true, "list_windows");
|
|
74
|
+
result["windows"] = windows;
|
|
75
|
+
result["truncated"] = truncated || identityChanged;
|
|
76
|
+
if(identityChanged){result["conclusive"]=false;result["observationError"]="WINDOW_REFERENCE_LOST: a lifetime changed during enumeration; incomplete observation";}
|
|
77
|
+
result["source"] = "win32";
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[MTAThread]
|
|
82
|
+
static void Main() {
|
|
83
|
+
Console.InputEncoding = new UTF8Encoding(false);
|
|
84
|
+
Console.OutputEncoding = new UTF8Encoding(false);
|
|
85
|
+
try { if (!SetProcessDpiAwarenessContext(new IntPtr(-4))) SetProcessDPIAware(); }
|
|
86
|
+
catch (EntryPointNotFoundException) { SetProcessDPIAware(); }
|
|
87
|
+
JavaScriptSerializer json = new JavaScriptSerializer { MaxJsonLength = 32 * 1024 * 1024 };
|
|
88
|
+
string line;
|
|
89
|
+
while ((line = Console.ReadLine()) != null) {
|
|
90
|
+
Dictionary<string, object> request = null;
|
|
91
|
+
try {
|
|
92
|
+
request = json.Deserialize<Dictionary<string, object>>(line);
|
|
93
|
+
if (request.ContainsKey("cancel")) continue;
|
|
94
|
+
string action = Convert.ToString(request["action"]);
|
|
95
|
+
currentReference=null;windowDispatchAttempted=false;
|
|
96
|
+
pointerInputAction=action=="click" || action=="double_click" || action=="move" || action=="drag" || action=="scroll" || action=="type_text" || action=="keypress";
|
|
97
|
+
pointerOperation=request.ContainsKey("operationId")?Convert.ToString(request["operationId"]):"";
|
|
98
|
+
object result;
|
|
99
|
+
try {
|
|
100
|
+
Dictionary<string, object> args = (Dictionary<string, object>)request["args"];
|
|
101
|
+
if (action == "type_text" || action == "keypress") result = ExecuteText(action,args);
|
|
102
|
+
else if (action == "click" || action == "double_click" || action == "move" || action == "drag" || (action == "scroll" && (!args.ContainsKey("elementId") || (args.ContainsKey("_cancelEvent") && !Option(args,"_quietScroll",false))))) result = ExecutePointer(action,args);
|
|
103
|
+
else if (action == "list_windows") result = ListWindows(args);
|
|
104
|
+
else if (action == "browser_frame") result = MarkBrowserFrame(args);
|
|
105
|
+
else if (action == "focus" || action == "invoke" || action == "set_value" || action == "scroll") result = ExecuteControl(action,args);
|
|
106
|
+
else if (action == "launch_app" || action == "activate_window" || action == "move_window" || action == "close_window") result = ManageNativeWindow(action,args);
|
|
107
|
+
else if (action == "screenshot") result = CaptureScreenshot(args);
|
|
108
|
+
else if (action == "snapshot" || action == "accessibility_tree" || action == "find" || action == "element_info") result = ObserveAccessibility(action, args);
|
|
109
|
+
else throw new InvalidOperationException("UNKNOWN_ACTION");
|
|
110
|
+
} catch (Exception error) {
|
|
111
|
+
Dictionary<string, object> failure = Result(false, action);
|
|
112
|
+
failure["error"] = "WINDOWS_ERROR: " + error.Message;
|
|
113
|
+
ActivationRefusedException refused=error as ActivationRefusedException;
|
|
114
|
+
if(refused!=null)failure["activationDiagnostic"]=refused.Diagnostic;
|
|
115
|
+
result = failure;
|
|
116
|
+
}
|
|
117
|
+
var payload=result as Dictionary<string,object>;
|
|
118
|
+
if(payload!=null)FinishWindowReference(payload);
|
|
119
|
+
Console.WriteLine(json.Serialize(new Dictionary<string, object> { { "id", request["id"] }, { "result", result } }));
|
|
120
|
+
Console.Out.Flush();
|
|
121
|
+
} catch (Exception error) {
|
|
122
|
+
Console.Error.WriteLine("NATIVE_PROTOCOL_ERROR: " + error.Message);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
CleanupWindowReferences();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { WorkerResult } from "./protocol.js";
|
|
2
|
+
type Property = {
|
|
3
|
+
type: "string" | "integer" | "boolean";
|
|
4
|
+
default?: unknown;
|
|
5
|
+
enum?: readonly string[];
|
|
6
|
+
minimum?: number;
|
|
7
|
+
maximum?: number;
|
|
8
|
+
minLength?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare const observationTools: {
|
|
11
|
+
action: string;
|
|
12
|
+
parameters: {
|
|
13
|
+
properties: Record<string, Property>;
|
|
14
|
+
required?: string[];
|
|
15
|
+
type: string;
|
|
16
|
+
additionalProperties: boolean;
|
|
17
|
+
};
|
|
18
|
+
}[];
|
|
19
|
+
export declare const controlTools: {
|
|
20
|
+
action: string;
|
|
21
|
+
parameters: {
|
|
22
|
+
properties: Record<string, Property>;
|
|
23
|
+
required?: string[];
|
|
24
|
+
type: string;
|
|
25
|
+
additionalProperties: boolean;
|
|
26
|
+
};
|
|
27
|
+
}[];
|
|
28
|
+
export declare function executeAccessibility(action: string, args: Record<string, unknown>, signal?: AbortSignal, execute?: (normalized: Record<string, unknown>) => Promise<WorkerResult>): Promise<WorkerResult>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { screenshot } from "./capture.js";
|
|
2
|
+
import { nativeDesktop } from "./native.js";
|
|
3
|
+
import { ocrDesktop } from "./ocr.js";
|
|
4
|
+
const target = {
|
|
5
|
+
windowRef: { type: "string", minLength: 1 },
|
|
6
|
+
windowTitle: { type: "string" },
|
|
7
|
+
processId: { type: "integer", minimum: 1 },
|
|
8
|
+
nativeWindowHandle: { type: "integer", minimum: 1 },
|
|
9
|
+
activate: { type: "boolean" },
|
|
10
|
+
};
|
|
11
|
+
const view = {
|
|
12
|
+
viewMode: {
|
|
13
|
+
type: "string",
|
|
14
|
+
enum: ["control", "content", "raw"],
|
|
15
|
+
default: "control",
|
|
16
|
+
},
|
|
17
|
+
includeOffscreen: { type: "boolean", default: false },
|
|
18
|
+
};
|
|
19
|
+
const browser = {
|
|
20
|
+
browserTab: { type: "string", minLength: 1 },
|
|
21
|
+
};
|
|
22
|
+
const detail = {
|
|
23
|
+
detailLevel: {
|
|
24
|
+
type: "string",
|
|
25
|
+
enum: ["compact", "full"],
|
|
26
|
+
default: "compact",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const tree = {
|
|
30
|
+
scope: {
|
|
31
|
+
type: "string",
|
|
32
|
+
enum: ["active_window", "desktop"],
|
|
33
|
+
default: "active_window",
|
|
34
|
+
},
|
|
35
|
+
treeTimeoutMs: {
|
|
36
|
+
type: "integer",
|
|
37
|
+
minimum: 100,
|
|
38
|
+
maximum: 30000,
|
|
39
|
+
default: 5000,
|
|
40
|
+
},
|
|
41
|
+
maxDepth: { type: "integer", minimum: 0, maximum: 12, default: 6 },
|
|
42
|
+
maxNodes: { type: "integer", minimum: 1, maximum: 3000, default: 500 },
|
|
43
|
+
};
|
|
44
|
+
const definitions = {
|
|
45
|
+
accessibility_tree: {
|
|
46
|
+
properties: { ...target, ...browser, ...view, ...detail, ...tree },
|
|
47
|
+
},
|
|
48
|
+
snapshot: {
|
|
49
|
+
properties: {
|
|
50
|
+
...target,
|
|
51
|
+
...browser,
|
|
52
|
+
...view,
|
|
53
|
+
...detail,
|
|
54
|
+
...tree,
|
|
55
|
+
maxDepth: { ...tree.maxDepth, default: 5 },
|
|
56
|
+
maxNodes: { ...tree.maxNodes, maximum: 2000, default: 250 },
|
|
57
|
+
includeScreenshot: { type: "boolean", default: true },
|
|
58
|
+
captureWindow: { type: "boolean", default: false },
|
|
59
|
+
maxWidth: { type: "integer", minimum: 0, maximum: 7680, default: 1600 },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
find: {
|
|
63
|
+
properties: {
|
|
64
|
+
...target,
|
|
65
|
+
...browser,
|
|
66
|
+
...view,
|
|
67
|
+
...tree,
|
|
68
|
+
query: { type: "string", minLength: 1 },
|
|
69
|
+
controlType: { type: "string" },
|
|
70
|
+
ocrFallback: { type: "boolean", default: true },
|
|
71
|
+
maxDepth: { ...tree.maxDepth, default: 8 },
|
|
72
|
+
maxNodes: { ...tree.maxNodes, maximum: 5000, default: 1200 },
|
|
73
|
+
maxResults: { type: "integer", minimum: 1, maximum: 200, default: 25 },
|
|
74
|
+
},
|
|
75
|
+
required: ["query"],
|
|
76
|
+
},
|
|
77
|
+
element_info: {
|
|
78
|
+
properties: {
|
|
79
|
+
...target,
|
|
80
|
+
...view,
|
|
81
|
+
...detail,
|
|
82
|
+
elementId: { type: "string", minLength: 1 },
|
|
83
|
+
x: { type: "integer" },
|
|
84
|
+
y: { type: "integer" },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
focus: {
|
|
88
|
+
properties: {
|
|
89
|
+
...target,
|
|
90
|
+
...view,
|
|
91
|
+
elementId: { type: "string", minLength: 1 },
|
|
92
|
+
},
|
|
93
|
+
required: ["elementId"],
|
|
94
|
+
},
|
|
95
|
+
invoke: {
|
|
96
|
+
properties: {
|
|
97
|
+
...target,
|
|
98
|
+
...view,
|
|
99
|
+
elementId: { type: "string", minLength: 1 },
|
|
100
|
+
action: {
|
|
101
|
+
type: "string",
|
|
102
|
+
enum: ["default", "expand", "collapse", "scroll_into_view"],
|
|
103
|
+
default: "default",
|
|
104
|
+
},
|
|
105
|
+
fallbackClick: { type: "boolean", default: true },
|
|
106
|
+
},
|
|
107
|
+
required: ["elementId"],
|
|
108
|
+
},
|
|
109
|
+
set_value: {
|
|
110
|
+
properties: {
|
|
111
|
+
...target,
|
|
112
|
+
...view,
|
|
113
|
+
elementId: { type: "string", minLength: 1 },
|
|
114
|
+
value: { type: "string" },
|
|
115
|
+
fallbackType: { type: "boolean", default: true },
|
|
116
|
+
restoreClipboard: { type: "boolean", default: true },
|
|
117
|
+
},
|
|
118
|
+
required: ["elementId", "value"],
|
|
119
|
+
},
|
|
120
|
+
scroll: {
|
|
121
|
+
properties: {
|
|
122
|
+
...target,
|
|
123
|
+
...view,
|
|
124
|
+
...browser,
|
|
125
|
+
elementId: { type: "string", minLength: 1 },
|
|
126
|
+
x: { type: "integer" },
|
|
127
|
+
y: { type: "integer" },
|
|
128
|
+
deltaY: { type: "integer", default: 480 },
|
|
129
|
+
deltaX: { type: "integer", default: 0 },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const toolDefinitions = Object.entries(definitions).map(([action, definition]) => ({
|
|
134
|
+
action,
|
|
135
|
+
parameters: { type: "object", additionalProperties: false, ...definition },
|
|
136
|
+
}));
|
|
137
|
+
export const observationTools = toolDefinitions.filter((tool) => ["snapshot", "accessibility_tree", "find", "element_info"].includes(tool.action));
|
|
138
|
+
export const controlTools = toolDefinitions.filter((tool) => ["focus", "invoke", "set_value", "scroll"].includes(tool.action));
|
|
139
|
+
export async function executeAccessibility(action, args, signal, execute) {
|
|
140
|
+
const definition = definitions[action];
|
|
141
|
+
if (!definition)
|
|
142
|
+
return { ok: false, action, error: "UNKNOWN_ACTION" };
|
|
143
|
+
const normalized = {};
|
|
144
|
+
const invalid = (message) => ({
|
|
145
|
+
ok: false,
|
|
146
|
+
action,
|
|
147
|
+
error: `INVALID_ARGUMENTS: ${message}`,
|
|
148
|
+
});
|
|
149
|
+
for (const key of Object.keys(args))
|
|
150
|
+
if (!(key in definition.properties))
|
|
151
|
+
return invalid(`unknown option ${key}`);
|
|
152
|
+
for (const [key, rule] of Object.entries(definition.properties)) {
|
|
153
|
+
const value = Object.hasOwn(args, key) ? args[key] : rule.default;
|
|
154
|
+
if (value === undefined) {
|
|
155
|
+
if (definition.required?.includes(key))
|
|
156
|
+
return invalid(`${key} is required`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (rule.type === "integer"
|
|
160
|
+
? !Number.isSafeInteger(value)
|
|
161
|
+
: typeof value !== rule.type)
|
|
162
|
+
return invalid(`${key} must be ${rule.type}`);
|
|
163
|
+
if (typeof value === "number" &&
|
|
164
|
+
((rule.minimum !== undefined && value < rule.minimum) ||
|
|
165
|
+
(rule.maximum !== undefined && value > rule.maximum)))
|
|
166
|
+
return invalid(`${key} is outside supported bounds`);
|
|
167
|
+
if (typeof value === "string" &&
|
|
168
|
+
((rule.enum && !rule.enum.includes(value)) ||
|
|
169
|
+
(rule.minLength && value.length < rule.minLength)))
|
|
170
|
+
return invalid(`${key} has an unsupported value`);
|
|
171
|
+
normalized[key] = value;
|
|
172
|
+
}
|
|
173
|
+
if (action === "element_info" &&
|
|
174
|
+
!normalized.elementId &&
|
|
175
|
+
(normalized.x === undefined || normalized.y === undefined))
|
|
176
|
+
return invalid("provide elementId or both x and y");
|
|
177
|
+
if (action === "scroll" && !normalized.elementId) {
|
|
178
|
+
if (normalized.x === undefined || normalized.y === undefined)
|
|
179
|
+
return invalid("provide elementId or both x and y");
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
action,
|
|
183
|
+
error: "INPUT_SAFETY_NOT_READY: coordinate scrolling requires guarded physical input",
|
|
184
|
+
inputRequired: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
signal?.throwIfAborted();
|
|
188
|
+
let result;
|
|
189
|
+
// Guarded mutation failures retain the cleanup lease; do not translate them
|
|
190
|
+
// into ordinary observation errors or clear the outer input state.
|
|
191
|
+
if (execute)
|
|
192
|
+
return execute(normalized);
|
|
193
|
+
try {
|
|
194
|
+
result = await nativeDesktop.execute(action, normalized, signal);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
result = {
|
|
198
|
+
ok: false,
|
|
199
|
+
action,
|
|
200
|
+
error: error instanceof Error ? error.message : String(error),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (action === "snapshot" && normalized.includeScreenshot) {
|
|
204
|
+
delete result.screenshotPending;
|
|
205
|
+
const windowCapture = normalized.captureWindow && normalized.scope !== "desktop";
|
|
206
|
+
const captureArgs = {
|
|
207
|
+
scope: windowCapture ? "active_window" : "desktop",
|
|
208
|
+
maxWidth: normalized.maxWidth,
|
|
209
|
+
};
|
|
210
|
+
if (windowCapture) {
|
|
211
|
+
for (const key of [
|
|
212
|
+
"windowRef",
|
|
213
|
+
"windowTitle",
|
|
214
|
+
"processId",
|
|
215
|
+
"nativeWindowHandle",
|
|
216
|
+
"activate",
|
|
217
|
+
]) {
|
|
218
|
+
const value = result[key] ?? normalized[key];
|
|
219
|
+
if (value !== undefined)
|
|
220
|
+
captureArgs[key] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const captured = await screenshot(captureArgs, signal);
|
|
224
|
+
if (captured.ok && captured.screenshot)
|
|
225
|
+
result.screenshot = captured.screenshot;
|
|
226
|
+
else {
|
|
227
|
+
result.partial = true;
|
|
228
|
+
result.screenshotError = String(captured.error ?? "SCREENSHOT_UNAVAILABLE");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (action === "find" && result.ocrFallbackPending) {
|
|
232
|
+
delete result.ocrFallbackPending;
|
|
233
|
+
const ocrArgs = {
|
|
234
|
+
query: normalized.query,
|
|
235
|
+
scope: normalized.scope,
|
|
236
|
+
};
|
|
237
|
+
for (const key of [
|
|
238
|
+
"windowRef",
|
|
239
|
+
"windowTitle",
|
|
240
|
+
"processId",
|
|
241
|
+
"nativeWindowHandle",
|
|
242
|
+
"activate",
|
|
243
|
+
]) {
|
|
244
|
+
const value = result[key] ?? normalized[key];
|
|
245
|
+
if (value !== undefined)
|
|
246
|
+
ocrArgs[key] = value;
|
|
247
|
+
}
|
|
248
|
+
const recognized = await ocrDesktop(ocrArgs, signal);
|
|
249
|
+
result.ocr = recognized;
|
|
250
|
+
if (recognized.ok)
|
|
251
|
+
result.ocrCandidates = recognized.matched ?? [];
|
|
252
|
+
else {
|
|
253
|
+
result.ocrError = recognized.error;
|
|
254
|
+
result.conclusive = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type Rule = {
|
|
2
|
+
type: string;
|
|
3
|
+
default?: unknown;
|
|
4
|
+
enum?: string[];
|
|
5
|
+
minimum?: number;
|
|
6
|
+
maximum?: number;
|
|
7
|
+
minLength?: number;
|
|
8
|
+
minItems?: number;
|
|
9
|
+
items?: Rule;
|
|
10
|
+
};
|
|
11
|
+
export declare const browserActionTools: {
|
|
12
|
+
action: string;
|
|
13
|
+
parameters: {
|
|
14
|
+
properties: Record<string, Rule>;
|
|
15
|
+
required?: string[];
|
|
16
|
+
type: string;
|
|
17
|
+
additionalProperties: boolean;
|
|
18
|
+
};
|
|
19
|
+
}[];
|
|
20
|
+
export declare function browserActionRefusal(action: string, args: Record<string, unknown>): string | undefined;
|
|
21
|
+
export declare function normalizeBrowserAction(action: string, args: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { timingMilliseconds } from "./input-timing.js";
|
|
2
|
+
// Shared reference-compatible tool schemas; browser execution stays in browser.ts.
|
|
3
|
+
const target = {
|
|
4
|
+
windowRef: { type: "string", minLength: 1 },
|
|
5
|
+
windowTitle: { type: "string" },
|
|
6
|
+
processId: { type: "integer", minimum: 1 },
|
|
7
|
+
nativeWindowHandle: { type: "integer", minimum: 1 },
|
|
8
|
+
activate: { type: "boolean", default: false },
|
|
9
|
+
};
|
|
10
|
+
const view = {
|
|
11
|
+
viewMode: {
|
|
12
|
+
type: "string",
|
|
13
|
+
enum: ["control", "content", "raw"],
|
|
14
|
+
default: "control",
|
|
15
|
+
},
|
|
16
|
+
includeOffscreen: { type: "boolean", default: false },
|
|
17
|
+
};
|
|
18
|
+
const tab = { browserTab: { type: "string", minLength: 1 } };
|
|
19
|
+
const element = { elementId: { type: "string", minLength: 1 } };
|
|
20
|
+
const point = { x: { type: "integer" }, y: { type: "integer" } };
|
|
21
|
+
const button = {
|
|
22
|
+
type: "string",
|
|
23
|
+
enum: ["left", "right", "middle"],
|
|
24
|
+
default: "left",
|
|
25
|
+
};
|
|
26
|
+
const dispatch = {
|
|
27
|
+
type: "string",
|
|
28
|
+
enum: ["auto", "background", "foreground"],
|
|
29
|
+
default: "auto",
|
|
30
|
+
};
|
|
31
|
+
const definitions = {
|
|
32
|
+
click: {
|
|
33
|
+
properties: {
|
|
34
|
+
...target,
|
|
35
|
+
...view,
|
|
36
|
+
...element,
|
|
37
|
+
...point,
|
|
38
|
+
button,
|
|
39
|
+
dispatch,
|
|
40
|
+
holdMs: timingMilliseconds,
|
|
41
|
+
timeoutMs: timingMilliseconds,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
double_click: {
|
|
45
|
+
properties: { ...target, ...view, ...element, ...point, button, dispatch },
|
|
46
|
+
},
|
|
47
|
+
move: { properties: { ...target, ...view, ...element, ...point } },
|
|
48
|
+
scroll: {
|
|
49
|
+
properties: {
|
|
50
|
+
...target,
|
|
51
|
+
...view,
|
|
52
|
+
...tab,
|
|
53
|
+
...element,
|
|
54
|
+
...point,
|
|
55
|
+
deltaY: { type: "integer", default: 480 },
|
|
56
|
+
deltaX: { type: "integer", default: 0 },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
type_text: {
|
|
60
|
+
required: ["text"],
|
|
61
|
+
properties: {
|
|
62
|
+
...target,
|
|
63
|
+
...tab,
|
|
64
|
+
text: { type: "string" },
|
|
65
|
+
method: {
|
|
66
|
+
type: "string",
|
|
67
|
+
enum: ["auto", "clipboard", "sendinput", "background"],
|
|
68
|
+
default: "auto",
|
|
69
|
+
},
|
|
70
|
+
focusMode: {
|
|
71
|
+
type: "string",
|
|
72
|
+
enum: ["preserve", "main"],
|
|
73
|
+
default: "preserve",
|
|
74
|
+
},
|
|
75
|
+
restoreClipboard: { type: "boolean", default: true },
|
|
76
|
+
inputDelayMs: timingMilliseconds,
|
|
77
|
+
timeoutMs: timingMilliseconds,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
keypress: {
|
|
81
|
+
required: ["keys"],
|
|
82
|
+
properties: {
|
|
83
|
+
holdMs: timingMilliseconds,
|
|
84
|
+
timeoutMs: timingMilliseconds,
|
|
85
|
+
...target,
|
|
86
|
+
...tab,
|
|
87
|
+
keys: {
|
|
88
|
+
type: "array",
|
|
89
|
+
minItems: 1,
|
|
90
|
+
items: { type: "string", minLength: 1 },
|
|
91
|
+
},
|
|
92
|
+
dispatch: {
|
|
93
|
+
type: "string",
|
|
94
|
+
enum: ["foreground", "background"],
|
|
95
|
+
default: "foreground",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
focus: {
|
|
100
|
+
required: ["elementId"],
|
|
101
|
+
properties: { ...target, ...view, ...element },
|
|
102
|
+
},
|
|
103
|
+
invoke: {
|
|
104
|
+
required: ["elementId"],
|
|
105
|
+
properties: {
|
|
106
|
+
...target,
|
|
107
|
+
...view,
|
|
108
|
+
...element,
|
|
109
|
+
action: {
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: ["default", "expand", "collapse", "scroll_into_view"],
|
|
112
|
+
default: "default",
|
|
113
|
+
},
|
|
114
|
+
fallbackClick: { type: "boolean", default: true },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
set_value: {
|
|
118
|
+
required: ["elementId", "value"],
|
|
119
|
+
properties: {
|
|
120
|
+
...target,
|
|
121
|
+
...view,
|
|
122
|
+
...element,
|
|
123
|
+
value: { type: "string" },
|
|
124
|
+
fallbackType: { type: "boolean", default: true },
|
|
125
|
+
restoreClipboard: { type: "boolean", default: true },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
activate_window: { properties: { ...target, ...tab } },
|
|
129
|
+
close_window: { properties: { ...target, ...tab } },
|
|
130
|
+
wait_for: {
|
|
131
|
+
properties: {
|
|
132
|
+
...target,
|
|
133
|
+
...tab,
|
|
134
|
+
...view,
|
|
135
|
+
condition: {
|
|
136
|
+
type: "string",
|
|
137
|
+
enum: ["window", "element", "text"],
|
|
138
|
+
default: "window",
|
|
139
|
+
},
|
|
140
|
+
query: { type: "string" },
|
|
141
|
+
controlType: { type: "string" },
|
|
142
|
+
scope: {
|
|
143
|
+
type: "string",
|
|
144
|
+
enum: ["active_window", "desktop"],
|
|
145
|
+
default: "active_window",
|
|
146
|
+
},
|
|
147
|
+
treeTimeoutMs: {
|
|
148
|
+
type: "integer",
|
|
149
|
+
minimum: 100,
|
|
150
|
+
maximum: 30000,
|
|
151
|
+
default: 1000,
|
|
152
|
+
},
|
|
153
|
+
maxDepth: { type: "integer", minimum: 0, maximum: 12, default: 8 },
|
|
154
|
+
maxNodes: { type: "integer", minimum: 1, maximum: 5000, default: 1200 },
|
|
155
|
+
appear: { type: "boolean", default: true },
|
|
156
|
+
timeoutMs: {
|
|
157
|
+
type: "integer",
|
|
158
|
+
minimum: 100,
|
|
159
|
+
maximum: 60000,
|
|
160
|
+
default: 5000,
|
|
161
|
+
},
|
|
162
|
+
intervalMs: { type: "integer", minimum: 50, maximum: 5000, default: 250 },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
export const browserActionTools = Object.entries(definitions).map(([action, definition]) => ({
|
|
167
|
+
action,
|
|
168
|
+
parameters: { type: "object", additionalProperties: false, ...definition },
|
|
169
|
+
}));
|
|
170
|
+
export function browserActionRefusal(action, args) {
|
|
171
|
+
if (action === "click" &&
|
|
172
|
+
(args.holdMs !== undefined || args.timeoutMs !== undefined))
|
|
173
|
+
return "TIMING_UNSUPPORTED: click timing requires native foreground dispatch";
|
|
174
|
+
if (action === "keypress" &&
|
|
175
|
+
(args.holdMs !== undefined ||
|
|
176
|
+
args.timeoutMs !== undefined ||
|
|
177
|
+
(Array.isArray(args.keys) &&
|
|
178
|
+
args.keys.length === 1 &&
|
|
179
|
+
/^(ctrl|control|alt|option|shift)$/i.test(String(args.keys[0]).trim()))))
|
|
180
|
+
return "TIMING_UNSUPPORTED: keypress timing and standalone modifiers require native foreground dispatch";
|
|
181
|
+
if (action === "type_text" &&
|
|
182
|
+
(args.inputDelayMs !== undefined || args.timeoutMs !== undefined))
|
|
183
|
+
return "TIMING_UNSUPPORTED: text timing requires explicit native sendinput";
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
export function normalizeBrowserAction(action, args) {
|
|
187
|
+
const refusal = browserActionRefusal(action, args);
|
|
188
|
+
if (refusal)
|
|
189
|
+
throw new Error(refusal);
|
|
190
|
+
const definition = definitions[action];
|
|
191
|
+
if (!definition)
|
|
192
|
+
return undefined;
|
|
193
|
+
const properties = { ...definition.properties, ...tab };
|
|
194
|
+
for (const key of Object.keys(args))
|
|
195
|
+
if (!Object.hasOwn(properties, key))
|
|
196
|
+
throw new Error(`INVALID_ARGUMENTS: unknown option ${key}`);
|
|
197
|
+
const normalized = {};
|
|
198
|
+
const check = (name, value, rule) => {
|
|
199
|
+
if (rule.type === "array") {
|
|
200
|
+
if (!Array.isArray(value) ||
|
|
201
|
+
value.length < (rule.minItems ?? 0) ||
|
|
202
|
+
value.length > 10000)
|
|
203
|
+
throw new Error(`INVALID_ARGUMENTS: ${name} must be an array with supported length`);
|
|
204
|
+
if (rule.items)
|
|
205
|
+
for (const child of value)
|
|
206
|
+
check(name, child, rule.items);
|
|
207
|
+
}
|
|
208
|
+
else if (rule.type === "integer"
|
|
209
|
+
? !Number.isSafeInteger(value)
|
|
210
|
+
: typeof value !== rule.type)
|
|
211
|
+
throw new Error(`INVALID_ARGUMENTS: ${name} must be ${rule.type}`);
|
|
212
|
+
if ((typeof value === "number" &&
|
|
213
|
+
((rule.minimum !== undefined && value < rule.minimum) ||
|
|
214
|
+
(rule.maximum !== undefined && value > rule.maximum))) ||
|
|
215
|
+
(typeof value === "string" &&
|
|
216
|
+
((rule.enum && !rule.enum.includes(value)) ||
|
|
217
|
+
(rule.minLength !== undefined && value.length < rule.minLength))))
|
|
218
|
+
throw new Error(`INVALID_ARGUMENTS: ${name} has an unsupported value`);
|
|
219
|
+
};
|
|
220
|
+
for (const [key, rule] of Object.entries(properties)) {
|
|
221
|
+
const value = Object.hasOwn(args, key) ? args[key] : rule.default;
|
|
222
|
+
if (value === undefined) {
|
|
223
|
+
if (definition.required?.includes(key))
|
|
224
|
+
throw new Error(`INVALID_ARGUMENTS: ${key} is required`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
check(key, value, rule);
|
|
228
|
+
normalized[key] = value;
|
|
229
|
+
}
|
|
230
|
+
if (action === "keypress") {
|
|
231
|
+
const keys = normalized.keys;
|
|
232
|
+
if (keys.some((key) => /^(win|windows|meta|cmd|command)$/i.test(key)))
|
|
233
|
+
throw new Error("INVALID_ARGUMENTS: Windows key is blocked");
|
|
234
|
+
if (keys.filter((key) => !/^(ctrl|control|alt|option|shift)$/i.test(key))
|
|
235
|
+
.length !== 1)
|
|
236
|
+
throw new Error("INVALID_ARGUMENTS: keypress needs exactly one non-modifier key");
|
|
237
|
+
}
|
|
238
|
+
if (action === "invoke" && normalized.action !== "default")
|
|
239
|
+
throw new Error("INVALID_ARGUMENTS: non-default invoke actions apply only to UI Automation elements");
|
|
240
|
+
if (action === "wait_for" &&
|
|
241
|
+
(normalized.condition === "window" ||
|
|
242
|
+
typeof normalized.query !== "string" ||
|
|
243
|
+
!normalized.query.trim()))
|
|
244
|
+
throw new Error("INVALID_ARGUMENTS: browser wait_for requires element/text condition and query");
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|