@crewhaus/tool-mouse-keyboard 0.1.0
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/package.json +45 -0
- package/src/index.test.ts +118 -0
- package/src/index.ts +159 -0
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/tool-mouse-keyboard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Click / Type / Key / Scroll tools — destructive: true (Section 25 BROW)",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@crewhaus/computer-use-driver": "0.0.0",
|
|
16
|
+
"@crewhaus/errors": "0.0.0",
|
|
17
|
+
"@crewhaus/tool-builder": "0.0.0",
|
|
18
|
+
"@crewhaus/tool-catalog": "0.0.0",
|
|
19
|
+
"zod": "^3.23.8"
|
|
20
|
+
},
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "Max Meier",
|
|
24
|
+
"email": "max@studiomax.io",
|
|
25
|
+
"url": "https://studiomax.io"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
30
|
+
"directory": "packages/tool-mouse-keyboard"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/tool-mouse-keyboard#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "restricted"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"NOTICE"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Driver } from "@crewhaus/computer-use-driver";
|
|
3
|
+
import {
|
|
4
|
+
createAllMouseKeyboardTools,
|
|
5
|
+
createClickTool,
|
|
6
|
+
createKeyTool,
|
|
7
|
+
createScrollTool,
|
|
8
|
+
createTypeTool,
|
|
9
|
+
} from "./index.js";
|
|
10
|
+
|
|
11
|
+
function recordingDriver(): { driver: Driver; calls: string[] } {
|
|
12
|
+
const calls: string[] = [];
|
|
13
|
+
const driver: Driver = {
|
|
14
|
+
backend: "chromium",
|
|
15
|
+
async connect() {},
|
|
16
|
+
async goto() {},
|
|
17
|
+
async screenshot() {
|
|
18
|
+
return new Uint8Array();
|
|
19
|
+
},
|
|
20
|
+
async click(x, y, button = "left") {
|
|
21
|
+
calls.push(`click ${button} ${x} ${y}`);
|
|
22
|
+
},
|
|
23
|
+
async type(text) {
|
|
24
|
+
calls.push(`type ${text}`);
|
|
25
|
+
},
|
|
26
|
+
async key(combo) {
|
|
27
|
+
calls.push(`key ${combo}`);
|
|
28
|
+
},
|
|
29
|
+
async scroll(dx, dy) {
|
|
30
|
+
calls.push(`scroll ${dx} ${dy}`);
|
|
31
|
+
},
|
|
32
|
+
async getViewport() {
|
|
33
|
+
return { width: 800, height: 600, devicePixelRatio: 1 };
|
|
34
|
+
},
|
|
35
|
+
async disconnect() {},
|
|
36
|
+
};
|
|
37
|
+
return { driver, calls };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("Mouse + keyboard tools (T1 wrapping)", () => {
|
|
41
|
+
test("Click forwards (x, y, button) to driver.click", async () => {
|
|
42
|
+
const { driver, calls } = recordingDriver();
|
|
43
|
+
const tool = createClickTool({ driver });
|
|
44
|
+
const r = await tool.execute({ x: 100, y: 200, button: "left" }, {});
|
|
45
|
+
expect(calls).toEqual(["click left 100 200"]);
|
|
46
|
+
expect(typeof r === "string" && r.includes("Clicked left at (100, 200)")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("Click default button is left", async () => {
|
|
50
|
+
const { driver, calls } = recordingDriver();
|
|
51
|
+
const tool = createClickTool({ driver });
|
|
52
|
+
await tool.execute({ x: 1, y: 2 }, {});
|
|
53
|
+
expect(calls).toEqual(["click left 1 2"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("Type forwards text", async () => {
|
|
57
|
+
const { driver, calls } = recordingDriver();
|
|
58
|
+
const tool = createTypeTool({ driver });
|
|
59
|
+
await tool.execute({ text: "hello" }, {});
|
|
60
|
+
expect(calls).toEqual(["type hello"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("Key forwards combo", async () => {
|
|
64
|
+
const { driver, calls } = recordingDriver();
|
|
65
|
+
const tool = createKeyTool({ driver });
|
|
66
|
+
await tool.execute({ combo: "Enter" }, {});
|
|
67
|
+
expect(calls).toEqual(["key Enter"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("Scroll forwards dx/dy", async () => {
|
|
71
|
+
const { driver, calls } = recordingDriver();
|
|
72
|
+
const tool = createScrollTool({ driver });
|
|
73
|
+
await tool.execute({ dx: 0, dy: 100 }, {});
|
|
74
|
+
expect(calls).toEqual(["scroll 0 100"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("All four tools are destructive: true (T8 — permission floor)", () => {
|
|
78
|
+
const { driver } = recordingDriver();
|
|
79
|
+
const tools = createAllMouseKeyboardTools({ driver });
|
|
80
|
+
expect(tools.click.destructive).toBe(true);
|
|
81
|
+
expect(tools.type.destructive).toBe(true);
|
|
82
|
+
expect(tools.key.destructive).toBe(true);
|
|
83
|
+
expect(tools.scroll.destructive).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("createAllMouseKeyboardTools returns the four named tools", () => {
|
|
87
|
+
const { driver } = recordingDriver();
|
|
88
|
+
const tools = createAllMouseKeyboardTools({ driver });
|
|
89
|
+
expect(tools.click.name).toBe("Click");
|
|
90
|
+
expect(tools.type.name).toBe("Type");
|
|
91
|
+
expect(tools.key.name).toBe("Key");
|
|
92
|
+
expect(tools.scroll.name).toBe("Scroll");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("driver errors are surfaced as tool result strings (not thrown)", async () => {
|
|
96
|
+
const driver: Driver = {
|
|
97
|
+
backend: "chromium",
|
|
98
|
+
async connect() {},
|
|
99
|
+
async goto() {},
|
|
100
|
+
async screenshot() {
|
|
101
|
+
return new Uint8Array();
|
|
102
|
+
},
|
|
103
|
+
async click() {
|
|
104
|
+
throw new Error("driver explosion");
|
|
105
|
+
},
|
|
106
|
+
async type() {},
|
|
107
|
+
async key() {},
|
|
108
|
+
async scroll() {},
|
|
109
|
+
async getViewport() {
|
|
110
|
+
return { width: 0, height: 0, devicePixelRatio: 1 };
|
|
111
|
+
},
|
|
112
|
+
async disconnect() {},
|
|
113
|
+
};
|
|
114
|
+
const tool = createClickTool({ driver });
|
|
115
|
+
const r = await tool.execute({ x: 1, y: 1 }, {});
|
|
116
|
+
expect(typeof r === "string" && r.includes("[Click error]")).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog R4 `tool-mouse-keyboard` — Section 25 BROW.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `computer-use-driver` mouse + keyboard methods 1:1 as
|
|
5
|
+
* registered tools. ALL FOUR are `destructive: true` so the §3
|
|
6
|
+
* permission-engine refuses to grant `allow` in default mode without
|
|
7
|
+
* an explicit `alwaysAllow` rule. The smoke's permission-floor probe
|
|
8
|
+
* exercises this path: same spec WITHOUT alwaysAllow rules → denial
|
|
9
|
+
* cites the destructive flag + missing rule.
|
|
10
|
+
*
|
|
11
|
+
* Tools:
|
|
12
|
+
* - `Click(x, y, button?)` — `left | right | middle` click.
|
|
13
|
+
* - `Type(text)` — typed input.
|
|
14
|
+
* - `Key(combo)` — special-key combos ("Enter", "Tab", "Control+a").
|
|
15
|
+
* - `Scroll(dx, dy)` — wheel scroll in pixels.
|
|
16
|
+
*/
|
|
17
|
+
import type { Driver, MouseButton } from "@crewhaus/computer-use-driver";
|
|
18
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
19
|
+
import { buildTool } from "@crewhaus/tool-builder";
|
|
20
|
+
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
|
|
23
|
+
export class MouseKeyboardError extends CrewhausError {
|
|
24
|
+
override readonly name = "MouseKeyboardError";
|
|
25
|
+
constructor(message: string, cause?: unknown) {
|
|
26
|
+
super("tool", message, cause);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CreateMouseKeyboardToolsOptions = {
|
|
31
|
+
readonly driver: Driver;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const clickSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
x: z.number().int().min(0),
|
|
37
|
+
y: z.number().int().min(0),
|
|
38
|
+
button: z.enum(["left", "right", "middle"]).optional(),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
|
|
42
|
+
const typeSchema = z.object({ text: z.string() }).strict();
|
|
43
|
+
|
|
44
|
+
const keySchema = z
|
|
45
|
+
.object({
|
|
46
|
+
combo: z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.describe(
|
|
50
|
+
"Single key or combo, e.g. 'Enter', 'Tab', 'Control+a'. Uses Playwright's key naming.",
|
|
51
|
+
),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
|
|
55
|
+
const scrollSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
dx: z.number().int(),
|
|
58
|
+
dy: z.number().int(),
|
|
59
|
+
})
|
|
60
|
+
.strict();
|
|
61
|
+
|
|
62
|
+
export function createClickTool(opts: CreateMouseKeyboardToolsOptions): RegisteredTool {
|
|
63
|
+
return buildTool({
|
|
64
|
+
name: "Click",
|
|
65
|
+
description:
|
|
66
|
+
"Click at viewport pixel coordinates (x, y). Defaults to left button. Use FindElement(description) to get coordinates from a natural-language target. DESTRUCTIVE: requires explicit alwaysAllow rule.",
|
|
67
|
+
inputSchema: clickSchema,
|
|
68
|
+
readOnly: false,
|
|
69
|
+
destructive: true,
|
|
70
|
+
concurrencySafe: false,
|
|
71
|
+
classifyOutput: false,
|
|
72
|
+
execute: async (input) => {
|
|
73
|
+
const button: MouseButton = input.button ?? "left";
|
|
74
|
+
try {
|
|
75
|
+
await opts.driver.click(input.x, input.y, button);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return `[Click error] ${(err as Error).message ?? String(err)}`;
|
|
78
|
+
}
|
|
79
|
+
return `Clicked ${button} at (${input.x}, ${input.y}).`;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createTypeTool(opts: CreateMouseKeyboardToolsOptions): RegisteredTool {
|
|
85
|
+
return buildTool({
|
|
86
|
+
name: "Type",
|
|
87
|
+
description:
|
|
88
|
+
"Type text at the focused input. Click an input first to focus it. DESTRUCTIVE: requires explicit alwaysAllow rule.",
|
|
89
|
+
inputSchema: typeSchema,
|
|
90
|
+
readOnly: false,
|
|
91
|
+
destructive: true,
|
|
92
|
+
concurrencySafe: false,
|
|
93
|
+
classifyOutput: false,
|
|
94
|
+
execute: async (input) => {
|
|
95
|
+
try {
|
|
96
|
+
await opts.driver.type(input.text);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return `[Type error] ${(err as Error).message ?? String(err)}`;
|
|
99
|
+
}
|
|
100
|
+
return `Typed ${input.text.length} chars.`;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createKeyTool(opts: CreateMouseKeyboardToolsOptions): RegisteredTool {
|
|
106
|
+
return buildTool({
|
|
107
|
+
name: "Key",
|
|
108
|
+
description:
|
|
109
|
+
"Press a single key or combo (e.g. 'Enter', 'Tab', 'Control+a'). Uses Playwright's key naming. DESTRUCTIVE: requires explicit alwaysAllow rule.",
|
|
110
|
+
inputSchema: keySchema,
|
|
111
|
+
readOnly: false,
|
|
112
|
+
destructive: true,
|
|
113
|
+
concurrencySafe: false,
|
|
114
|
+
classifyOutput: false,
|
|
115
|
+
execute: async (input) => {
|
|
116
|
+
try {
|
|
117
|
+
await opts.driver.key(input.combo);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return `[Key error] ${(err as Error).message ?? String(err)}`;
|
|
120
|
+
}
|
|
121
|
+
return `Pressed ${input.combo}.`;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createScrollTool(opts: CreateMouseKeyboardToolsOptions): RegisteredTool {
|
|
127
|
+
return buildTool({
|
|
128
|
+
name: "Scroll",
|
|
129
|
+
description:
|
|
130
|
+
"Scroll by pixel deltas. Positive dy scrolls down. DESTRUCTIVE: requires explicit alwaysAllow rule.",
|
|
131
|
+
inputSchema: scrollSchema,
|
|
132
|
+
readOnly: false,
|
|
133
|
+
destructive: true,
|
|
134
|
+
concurrencySafe: false,
|
|
135
|
+
classifyOutput: false,
|
|
136
|
+
execute: async (input) => {
|
|
137
|
+
try {
|
|
138
|
+
await opts.driver.scroll(input.dx, input.dy);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return `[Scroll error] ${(err as Error).message ?? String(err)}`;
|
|
141
|
+
}
|
|
142
|
+
return `Scrolled (${input.dx}, ${input.dy}).`;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function createAllMouseKeyboardTools(opts: CreateMouseKeyboardToolsOptions): {
|
|
148
|
+
click: RegisteredTool;
|
|
149
|
+
type: RegisteredTool;
|
|
150
|
+
key: RegisteredTool;
|
|
151
|
+
scroll: RegisteredTool;
|
|
152
|
+
} {
|
|
153
|
+
return {
|
|
154
|
+
click: createClickTool(opts),
|
|
155
|
+
type: createTypeTool(opts),
|
|
156
|
+
key: createKeyTool(opts),
|
|
157
|
+
scroll: createScrollTool(opts),
|
|
158
|
+
};
|
|
159
|
+
}
|