@moxxy/plugin-computer-control 0.26.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/LICENSE +21 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/shell.d.ts +56 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +189 -0
- package/dist/shell.js.map +1 -0
- package/dist/tools/applescript.d.ts +2 -0
- package/dist/tools/applescript.d.ts.map +1 -0
- package/dist/tools/applescript.js +38 -0
- package/dist/tools/applescript.js.map +1 -0
- package/dist/tools/click.d.ts +2 -0
- package/dist/tools/click.d.ts.map +1 -0
- package/dist/tools/click.js +50 -0
- package/dist/tools/click.js.map +1 -0
- package/dist/tools/clipboard.d.ts +2 -0
- package/dist/tools/clipboard.d.ts.map +1 -0
- package/dist/tools/clipboard.js +62 -0
- package/dist/tools/clipboard.js.map +1 -0
- package/dist/tools/key.d.ts +11 -0
- package/dist/tools/key.d.ts.map +1 -0
- package/dist/tools/key.js +135 -0
- package/dist/tools/key.js.map +1 -0
- package/dist/tools/open.d.ts +2 -0
- package/dist/tools/open.d.ts.map +1 -0
- package/dist/tools/open.js +76 -0
- package/dist/tools/open.js.map +1 -0
- package/dist/tools/screenshot.d.ts +2 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +162 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/type.d.ts +8 -0
- package/dist/tools/type.d.ts.map +1 -0
- package/dist/tools/type.js +57 -0
- package/dist/tools/type.js.map +1 -0
- package/package.json +65 -0
- package/skills/computer-control.md +159 -0
- package/src/index.ts +55 -0
- package/src/shell.test.ts +186 -0
- package/src/shell.ts +213 -0
- package/src/tools/applescript-serialize.test.ts +74 -0
- package/src/tools/applescript.ts +41 -0
- package/src/tools/click.ts +52 -0
- package/src/tools/clipboard.ts +63 -0
- package/src/tools/key.ts +147 -0
- package/src/tools/open.ts +81 -0
- package/src/tools/screenshot.ts +181 -0
- package/src/tools/type.ts +60 -0
- package/src/tools.test.ts +128 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { defineTool, MoxxyError, z } from '@moxxy/sdk';
|
|
2
|
+
import { ensureDarwin, procFailureCause, runProcess } from '../shell.js';
|
|
3
|
+
const MODIFIER_NAMES = ['cmd', 'shift', 'option', 'control'];
|
|
4
|
+
/**
|
|
5
|
+
* Named keys → macOS key codes used by AppleScript's
|
|
6
|
+
* `key code N` command. Subset focused on what an automation agent
|
|
7
|
+
* actually needs (navigation, editing, function keys). Letters and
|
|
8
|
+
* digits go through `keystroke "x" using ...` instead of `key code`.
|
|
9
|
+
*/
|
|
10
|
+
const KEY_CODES = {
|
|
11
|
+
return: 36,
|
|
12
|
+
enter: 36,
|
|
13
|
+
tab: 48,
|
|
14
|
+
space: 49,
|
|
15
|
+
escape: 53,
|
|
16
|
+
esc: 53,
|
|
17
|
+
delete: 51,
|
|
18
|
+
backspace: 51,
|
|
19
|
+
forward_delete: 117,
|
|
20
|
+
left: 123,
|
|
21
|
+
right: 124,
|
|
22
|
+
down: 125,
|
|
23
|
+
up: 126,
|
|
24
|
+
home: 115,
|
|
25
|
+
end: 119,
|
|
26
|
+
page_up: 116,
|
|
27
|
+
page_down: 121,
|
|
28
|
+
f1: 122,
|
|
29
|
+
f2: 120,
|
|
30
|
+
f3: 99,
|
|
31
|
+
f4: 118,
|
|
32
|
+
f5: 96,
|
|
33
|
+
f6: 97,
|
|
34
|
+
f7: 98,
|
|
35
|
+
f8: 100,
|
|
36
|
+
f9: 101,
|
|
37
|
+
f10: 109,
|
|
38
|
+
f11: 103,
|
|
39
|
+
f12: 111,
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Single literal whitespace chars → their key codes. A bare ' '/'\t'/'\r'/'\n'
|
|
43
|
+
* would otherwise be sent via `keystroke`, which types the character (wrong for
|
|
44
|
+
* modifier chords like ctrl+space). Mirrors the matching KEY_CODES entries.
|
|
45
|
+
*/
|
|
46
|
+
const WHITESPACE_KEY_CODES = {
|
|
47
|
+
' ': 49, // space
|
|
48
|
+
'\t': 48, // tab
|
|
49
|
+
'\r': 36, // return
|
|
50
|
+
'\n': 36, // return
|
|
51
|
+
};
|
|
52
|
+
export const keyTool = defineTool({
|
|
53
|
+
name: 'computer_key',
|
|
54
|
+
description: 'Send a single key chord with optional modifiers. Use this for shortcuts ' +
|
|
55
|
+
'(cmd+c, cmd+tab, cmd+shift+4) and named keys (return, tab, escape, arrows, ' +
|
|
56
|
+
'page_up/down, f1–f12). For typing arbitrary text, use computer_type.',
|
|
57
|
+
inputSchema: z.object({
|
|
58
|
+
key: z
|
|
59
|
+
.string()
|
|
60
|
+
.min(1)
|
|
61
|
+
.describe('A single character ("a", "1", "/") OR a named key from the catalog: ' +
|
|
62
|
+
Object.keys(KEY_CODES).join(', ') +
|
|
63
|
+
'.'),
|
|
64
|
+
modifiers: z
|
|
65
|
+
.array(z.enum(MODIFIER_NAMES))
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('Held modifiers. Common combos: ["cmd"] for cmd+key, ["cmd","shift"], ["control","option"].'),
|
|
68
|
+
}),
|
|
69
|
+
permission: { action: 'prompt' },
|
|
70
|
+
async handler({ key, modifiers }, ctx) {
|
|
71
|
+
ensureDarwin('computer_key');
|
|
72
|
+
const mods = modifiers ?? [];
|
|
73
|
+
const script = buildKeyScript(key, mods);
|
|
74
|
+
const proc = await runProcess('osascript', ['-e', script], {
|
|
75
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
76
|
+
timeoutMs: 10_000,
|
|
77
|
+
});
|
|
78
|
+
if (proc.exitCode !== 0) {
|
|
79
|
+
const cause = procFailureCause(proc, 10_000);
|
|
80
|
+
throw new MoxxyError({
|
|
81
|
+
code: 'TOOL_ERROR',
|
|
82
|
+
message: cause
|
|
83
|
+
? `key ${cause}`
|
|
84
|
+
: `key failed (exit ${proc.exitCode}): ${proc.stderr.trim() || '(check Accessibility permission)'}`,
|
|
85
|
+
context: { tool: 'computer_key', exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, key, modifiers: mods };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* Build the `osascript -e` body for a single key chord. Named keys go
|
|
93
|
+
* through `key code N`, single characters through `keystroke "x"`, and a
|
|
94
|
+
* multi-char unknown key throws. Pure (no I/O) so it can be unit-tested.
|
|
95
|
+
*/
|
|
96
|
+
export function buildKeyScript(key, modifiers) {
|
|
97
|
+
const usingClause = modifiers.length > 0 ? ` using {${modifiers.map(modifierClause).join(', ')}}` : '';
|
|
98
|
+
const lower = key.toLowerCase();
|
|
99
|
+
if (lower in KEY_CODES) {
|
|
100
|
+
return `tell application "System Events" to key code ${KEY_CODES[lower]}${usingClause}`;
|
|
101
|
+
}
|
|
102
|
+
// A single literal whitespace char (' ', '\t', '\r', '\n') keystroked with a
|
|
103
|
+
// held modifier types the character rather than pressing the key, breaking
|
|
104
|
+
// chords like ctrl+space. Route it to the corresponding key code instead.
|
|
105
|
+
if (key.length === 1) {
|
|
106
|
+
const wsCode = WHITESPACE_KEY_CODES[key];
|
|
107
|
+
if (wsCode !== undefined) {
|
|
108
|
+
return `tell application "System Events" to key code ${wsCode}${usingClause}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (key.length === 1) {
|
|
112
|
+
const literal = `"${key.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
113
|
+
return `tell application "System Events" to keystroke ${literal}${usingClause}`;
|
|
114
|
+
}
|
|
115
|
+
throw new MoxxyError({
|
|
116
|
+
code: 'TOOL_ERROR',
|
|
117
|
+
message: `unknown key "${key}". Use a single character or one of: ${Object.keys(KEY_CODES).join(', ')}.`,
|
|
118
|
+
context: { tool: 'computer_key', key },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function modifierClause(m) {
|
|
122
|
+
// AppleScript uses "command down", "shift down", "option down",
|
|
123
|
+
// "control down" — map our short names.
|
|
124
|
+
switch (m) {
|
|
125
|
+
case 'cmd':
|
|
126
|
+
return 'command down';
|
|
127
|
+
case 'shift':
|
|
128
|
+
return 'shift down';
|
|
129
|
+
case 'option':
|
|
130
|
+
return 'option down';
|
|
131
|
+
case 'control':
|
|
132
|
+
return 'control down';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key.js","sourceRoot":"","sources":["../../src/tools/key.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/tools/open.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ,8BA6EnB,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { defineTool, MoxxyError, z } from '@moxxy/sdk';
|
|
2
|
+
import { ensureDarwin, procFailureCause, runProcess } from '../shell.js';
|
|
3
|
+
export const openTool = defineTool({
|
|
4
|
+
name: 'computer_open',
|
|
5
|
+
description: 'Open a URL, file path, or .app bundle via macOS `open`. Use this to ' +
|
|
6
|
+
'launch / activate a specific app or jump to a web page. The model should ' +
|
|
7
|
+
'prefer this over typing into Spotlight when the target is known.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
target: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.describe('URL (https://...), file path (/Users/...), or app name (Safari). ' +
|
|
13
|
+
'For app names, prefer the `app` field — `target` is treated as a path.')
|
|
14
|
+
.optional(),
|
|
15
|
+
app: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.max(120)
|
|
19
|
+
.describe('Application name to activate (e.g. "Safari", "Visual Studio Code"). ' +
|
|
20
|
+
'When `target` is also set, the app opens `target` (e.g. open a file in VS Code).')
|
|
21
|
+
.optional(),
|
|
22
|
+
}),
|
|
23
|
+
permission: { action: 'prompt' },
|
|
24
|
+
async handler({ target, app }, ctx) {
|
|
25
|
+
ensureDarwin('computer_open');
|
|
26
|
+
if (!target && !app) {
|
|
27
|
+
throw new MoxxyError({
|
|
28
|
+
code: 'TOOL_ERROR',
|
|
29
|
+
message: 'computer_open: at least one of `target` or `app` is required',
|
|
30
|
+
context: { tool: 'computer_open' },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// A target/app beginning with '-' (e.g. '-g', '-n', '-W') is parsed by
|
|
34
|
+
// /usr/bin/open as an OPTION FLAG, not a path/URL/app — silently changing
|
|
35
|
+
// behavior (open in background, new instance) the user did not approve.
|
|
36
|
+
// Reject it: no legitimate path/URL/app name starts with a dash, and
|
|
37
|
+
// spawn is array-form so this is the only argument-injection vector.
|
|
38
|
+
for (const [field, value] of [
|
|
39
|
+
['target', target],
|
|
40
|
+
['app', app],
|
|
41
|
+
]) {
|
|
42
|
+
if (value !== undefined && value.startsWith('-')) {
|
|
43
|
+
throw new MoxxyError({
|
|
44
|
+
code: 'TOOL_ERROR',
|
|
45
|
+
message: `computer_open: \`${field}\` must not begin with '-' (would be parsed as an option flag)`,
|
|
46
|
+
context: { tool: 'computer_open', field },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const args = [];
|
|
51
|
+
if (app) {
|
|
52
|
+
args.push('-a', app);
|
|
53
|
+
}
|
|
54
|
+
// `--` ends option parsing so the positional target is treated strictly
|
|
55
|
+
// as a path/URL operand even if a future change relaxes the guard above.
|
|
56
|
+
if (target) {
|
|
57
|
+
args.push('--', target);
|
|
58
|
+
}
|
|
59
|
+
const proc = await runProcess('open', args, {
|
|
60
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
61
|
+
timeoutMs: 10_000,
|
|
62
|
+
});
|
|
63
|
+
if (proc.exitCode !== 0) {
|
|
64
|
+
const cause = procFailureCause(proc, 10_000);
|
|
65
|
+
throw new MoxxyError({
|
|
66
|
+
code: 'TOOL_ERROR',
|
|
67
|
+
message: cause
|
|
68
|
+
? `open ${cause}`
|
|
69
|
+
: `open failed (exit ${proc.exitCode}): ${proc.stderr.trim() || '(no error message)'}`,
|
|
70
|
+
context: { tool: 'computer_open', exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, app, target };
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
//# sourceMappingURL=open.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open.js","sourceRoot":"","sources":["../../src/tools/open.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/tools/screenshot.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,cAAc,8BAwJzB,CAAC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { defineTool, MoxxyError, z } from '@moxxy/sdk';
|
|
6
|
+
import { ensureDarwin, procFailureCause, runProcess } from '../shell.js';
|
|
7
|
+
const regionSchema = z.object({
|
|
8
|
+
x: z.number().int().min(0),
|
|
9
|
+
y: z.number().int().min(0),
|
|
10
|
+
width: z.number().int().positive(),
|
|
11
|
+
height: z.number().int().positive(),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Defaults aggressive on size — a Retina full-screen PNG can pass
|
|
15
|
+
* 5 MB base64-encoded and instantly blow the model's context window.
|
|
16
|
+
* 1280px JPEG @ q72 is ~150 KB for a typical desktop and still
|
|
17
|
+
* legible enough for the model to identify UI elements.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_MAX_DIM = 1280;
|
|
20
|
+
const DEFAULT_FORMAT = 'jpeg';
|
|
21
|
+
const DEFAULT_JPEG_QUALITY = 72;
|
|
22
|
+
/** Soft cap on the encoded image size in bytes. We refuse to return
|
|
23
|
+
* anything bigger than this even after the model asks for high-res —
|
|
24
|
+
* better to fail loudly than silently nuke the context. */
|
|
25
|
+
const MAX_BYTES = 1_500_000;
|
|
26
|
+
export const screenshotTool = defineTool({
|
|
27
|
+
name: 'computer_screenshot',
|
|
28
|
+
description: 'Take a screenshot of the macOS desktop. Returns base64-encoded image bytes ' +
|
|
29
|
+
'the model can see directly. By default the image is downscaled to 1280px ' +
|
|
30
|
+
'and JPEG-compressed so a single screenshot stays well under context — ' +
|
|
31
|
+
'override `maxDim` / `format` / `quality` only when you genuinely need full ' +
|
|
32
|
+
'resolution. macOS will prompt for Screen Recording permission on first use.',
|
|
33
|
+
inputSchema: z.object({
|
|
34
|
+
region: regionSchema
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Crop to a rectangle in screen-pixel coordinates (top-left origin). Omit for full screen.'),
|
|
37
|
+
maxDim: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.min(256)
|
|
41
|
+
.max(3840)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe(`Resize so the longest edge is <= this many pixels. Default ${DEFAULT_MAX_DIM}. ` +
|
|
44
|
+
'Lower = smaller payload + less context cost; raise only when you need pixel detail.'),
|
|
45
|
+
format: z
|
|
46
|
+
.enum(['jpeg', 'png'])
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(`Output format. Default "${DEFAULT_FORMAT}" (much smaller). Pick "png" only when ` +
|
|
49
|
+
'you need lossless (rare — JPEG is fine for UI screenshots).'),
|
|
50
|
+
quality: z
|
|
51
|
+
.number()
|
|
52
|
+
.int()
|
|
53
|
+
.min(40)
|
|
54
|
+
.max(100)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe(`JPEG quality (1-100). Default ${DEFAULT_JPEG_QUALITY}. Ignored for PNG output.`),
|
|
57
|
+
}),
|
|
58
|
+
permission: { action: 'prompt' },
|
|
59
|
+
async handler({ region, maxDim, format, quality }, ctx) {
|
|
60
|
+
ensureDarwin('computer_screenshot');
|
|
61
|
+
const fmt = format ?? DEFAULT_FORMAT;
|
|
62
|
+
const dim = maxDim ?? DEFAULT_MAX_DIM;
|
|
63
|
+
const q = quality ?? DEFAULT_JPEG_QUALITY;
|
|
64
|
+
// Always capture as PNG first (preserves color depth + no double
|
|
65
|
+
// compression), then let `sips` resize and convert in one pass.
|
|
66
|
+
// `-x` silences the camera-shutter sound.
|
|
67
|
+
// A random suffix guarantees uniqueness even for two captures in the
|
|
68
|
+
// same process within the same millisecond (pid+Date.now() alone can
|
|
69
|
+
// collide and cross-corrupt the temp files).
|
|
70
|
+
const uniq = randomUUID();
|
|
71
|
+
const captureTmp = path.join(os.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}-${uniq}.png`);
|
|
72
|
+
const captureArgs = ['-x', '-t', 'png'];
|
|
73
|
+
if (region) {
|
|
74
|
+
captureArgs.push('-R', `${region.x},${region.y},${region.width},${region.height}`);
|
|
75
|
+
}
|
|
76
|
+
captureArgs.push(captureTmp);
|
|
77
|
+
// Guarantee the original PNG (potentially several MB for a Retina full
|
|
78
|
+
// screen) is removed on EVERY exit path — success, throw, or a spawn
|
|
79
|
+
// reject (e.g. `sips` not on PATH) / mid-capture timeout that may have
|
|
80
|
+
// left a partial file. Without this, those failure paths leak the temp
|
|
81
|
+
// file in os.tmpdir() permanently and accumulate over repeated failures.
|
|
82
|
+
try {
|
|
83
|
+
const cap = await runProcess('screencapture', captureArgs, {
|
|
84
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
85
|
+
timeoutMs: 15_000,
|
|
86
|
+
});
|
|
87
|
+
if (cap.exitCode !== 0) {
|
|
88
|
+
const cause = procFailureCause(cap, 15_000);
|
|
89
|
+
throw new MoxxyError({
|
|
90
|
+
code: 'TOOL_ERROR',
|
|
91
|
+
message: cause
|
|
92
|
+
? `screencapture ${cause}`
|
|
93
|
+
: `screencapture failed (exit ${cap.exitCode}): ${cap.stderr.trim() || '(no stderr — likely Screen Recording permission missing — grant in System Settings → Privacy & Security)'}`,
|
|
94
|
+
context: { tool: 'computer_screenshot', exitCode: cap.exitCode, timedOut: cap.timedOut ? 1 : 0 },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Resize + format-convert in one sips call. `-Z N` fits within N
|
|
98
|
+
// on the longest edge while preserving aspect ratio. Output ext
|
|
99
|
+
// picks the format; format options apply when JPEG.
|
|
100
|
+
const outExt = fmt === 'jpeg' ? 'jpg' : 'png';
|
|
101
|
+
const outTmp = path.join(os.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}-${uniq}-out.${outExt}`);
|
|
102
|
+
const sipsArgs = [
|
|
103
|
+
'-Z',
|
|
104
|
+
String(dim),
|
|
105
|
+
'--setProperty',
|
|
106
|
+
'format',
|
|
107
|
+
fmt,
|
|
108
|
+
];
|
|
109
|
+
if (fmt === 'jpeg') {
|
|
110
|
+
sipsArgs.push('--setProperty', 'formatOptions', String(q));
|
|
111
|
+
}
|
|
112
|
+
sipsArgs.push(captureTmp, '--out', outTmp);
|
|
113
|
+
const sip = await runProcess('sips', sipsArgs, {
|
|
114
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
115
|
+
timeoutMs: 15_000,
|
|
116
|
+
});
|
|
117
|
+
if (sip.exitCode !== 0) {
|
|
118
|
+
await fs.rm(outTmp, { force: true });
|
|
119
|
+
const cause = procFailureCause(sip, 15_000);
|
|
120
|
+
throw new MoxxyError({
|
|
121
|
+
code: 'TOOL_ERROR',
|
|
122
|
+
message: cause
|
|
123
|
+
? `sips resize/convert ${cause}`
|
|
124
|
+
: `sips resize/convert failed (exit ${sip.exitCode}): ${sip.stderr.trim() || '(no error message)'}`,
|
|
125
|
+
context: { tool: 'computer_screenshot', exitCode: sip.exitCode, timedOut: sip.timedOut ? 1 : 0 },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const bytes = await fs.readFile(outTmp);
|
|
130
|
+
if (bytes.length > MAX_BYTES) {
|
|
131
|
+
throw new MoxxyError({
|
|
132
|
+
code: 'TOOL_ERROR',
|
|
133
|
+
message: `screenshot exceeded ${MAX_BYTES} bytes after compression (got ${bytes.length}). ` +
|
|
134
|
+
`Lower maxDim (currently ${dim}) or quality (currently ${q}), or pass a smaller region.`,
|
|
135
|
+
context: { tool: 'computer_screenshot', byteLength: bytes.length },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// The `{ mediaType, base64 }` pair is load-bearing, not decorative: the
|
|
139
|
+
// SDK's tool_result projection keys off exactly this shape to emit a
|
|
140
|
+
// provider `image` ContentBlock so the model SEES the pixels. Returning
|
|
141
|
+
// the bytes inside a stringified blob (the JSON.stringify fallback path)
|
|
142
|
+
// would reach the model as base64 TEXT it cannot decode. Extra fields
|
|
143
|
+
// are diagnostic only and ignored by the image projection.
|
|
144
|
+
return {
|
|
145
|
+
mediaType: fmt === 'jpeg' ? 'image/jpeg' : 'image/png',
|
|
146
|
+
base64: bytes.toString('base64'),
|
|
147
|
+
byteLength: bytes.length,
|
|
148
|
+
maxDim: dim,
|
|
149
|
+
format: fmt,
|
|
150
|
+
...(fmt === 'jpeg' ? { quality: q } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await fs.rm(outTmp, { force: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
await fs.rm(captureTmp, { force: true });
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
//# sourceMappingURL=screenshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/tools/screenshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const typeTool: import("@moxxy/sdk").ToolDef;
|
|
2
|
+
/**
|
|
3
|
+
* Serialize an arbitrary JS string into a valid AppleScript string
|
|
4
|
+
* literal. AppleScript string syntax: `"..."` with `\"` and `\\` as
|
|
5
|
+
* the only escapes; newlines are written as `" & return & "`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function toAppleScriptString(s: string): string;
|
|
8
|
+
//# sourceMappingURL=type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../src/tools/type.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ,8BAwCnB,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CASrD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineTool, MoxxyError, z } from '@moxxy/sdk';
|
|
2
|
+
import { ensureDarwin, procFailureCause, runProcess } from '../shell.js';
|
|
3
|
+
export const typeTool = defineTool({
|
|
4
|
+
name: 'computer_type',
|
|
5
|
+
description: 'Type a string into whatever has keyboard focus. Use AFTER clicking the ' +
|
|
6
|
+
'target field. Macros (cmd+C, etc.) belong in computer_key, not here — this ' +
|
|
7
|
+
'tool sends each character literally. Requires Accessibility permission.',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
text: z
|
|
10
|
+
.string()
|
|
11
|
+
.max(4000)
|
|
12
|
+
.describe('The literal text to type. Newlines and tabs are typed as-is. ' +
|
|
13
|
+
'Pre-existing focus is the target — click first if needed.'),
|
|
14
|
+
}),
|
|
15
|
+
permission: { action: 'prompt' },
|
|
16
|
+
async handler({ text }, ctx) {
|
|
17
|
+
ensureDarwin('computer_type');
|
|
18
|
+
if (text.length === 0)
|
|
19
|
+
return { ok: true, length: 0 };
|
|
20
|
+
// Serialize the text to an AppleScript string literal ourselves and
|
|
21
|
+
// pass it inline via `osascript -e`, so we never have to escape
|
|
22
|
+
// AppleScript quote rules at the shell layer.
|
|
23
|
+
const literal = toAppleScriptString(text);
|
|
24
|
+
const directScript = `tell application "System Events" to keystroke ${literal}`;
|
|
25
|
+
const proc = await runProcess('osascript', ['-e', directScript], {
|
|
26
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
27
|
+
timeoutMs: 30_000,
|
|
28
|
+
});
|
|
29
|
+
if (proc.exitCode !== 0) {
|
|
30
|
+
const cause = procFailureCause(proc, 30_000);
|
|
31
|
+
throw new MoxxyError({
|
|
32
|
+
code: 'TOOL_ERROR',
|
|
33
|
+
message: cause
|
|
34
|
+
? `type ${cause}`
|
|
35
|
+
: `type failed (exit ${proc.exitCode}): ${proc.stderr.trim() || '(check Accessibility permission)'}`,
|
|
36
|
+
context: { tool: 'computer_type', exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, length: text.length };
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Serialize an arbitrary JS string into a valid AppleScript string
|
|
44
|
+
* literal. AppleScript string syntax: `"..."` with `\"` and `\\` as
|
|
45
|
+
* the only escapes; newlines are written as `" & return & "`.
|
|
46
|
+
*/
|
|
47
|
+
export function toAppleScriptString(s) {
|
|
48
|
+
// Split on newlines to use `return` (AppleScript's CR constant) so
|
|
49
|
+
// a multi-line type call sends actual Enter keystrokes rather than a
|
|
50
|
+
// literal `\n` in one keystroke (which keystroke would refuse).
|
|
51
|
+
const parts = s.split('\n').map((line) => {
|
|
52
|
+
const escaped = line.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
53
|
+
return `"${escaped}"`;
|
|
54
|
+
});
|
|
55
|
+
return parts.join(' & return & ');
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type.js","sourceRoot":"","sources":["../../src/tools/type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;IACjC,IAAI,EAAE,eAAe;IACrB,WAAW,EACT,yEAAyE;QACzE,6EAA6E;QAC7E,yEAAyE;IAC3E,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;QACpB,IAAI,EAAE,CAAC;aACJ,MAAM,EAAE;aACR,GAAG,CAAC,IAAI,CAAC;aACT,QAAQ,CACP,+DAA+D;YAC7D,2DAA2D,CAC9D;KACJ,CAAC;IACF,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;IAChC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,GAAG;QACzB,YAAY,CAAC,eAAe,CAAC,CAAC;QAC9B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QACtD,oEAAoE;QACpE,gEAAgE;QAChE,8CAA8C;QAC9C,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;QAC1C,MAAM,YAAY,GAAG,iDAAiD,OAAO,EAAE,CAAC;QAChF,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE;YAC/D,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7C,SAAS,EAAE,MAAM;SAClB,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC7C,MAAM,IAAI,UAAU,CAAC;gBACnB,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE,KAAK;oBACZ,CAAC,CAAC,QAAQ,KAAK,EAAE;oBACjB,CAAC,CAAC,qBAAqB,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,kCAAkC,EAAE;gBACtG,OAAO,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;aAC7F,CAAC,CAAC;QACL,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IAC3C,CAAC;CACF,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAAS;IAC3C,mEAAmE;IACnE,qEAAqE;IACrE,gEAAgE;IAChE,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACvC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACjE,OAAO,IAAI,OAAO,GAAG,CAAC;IACxB,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AACpC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moxxy/plugin-computer-control",
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "Programmatic control of the host computer (macOS only for now): screenshot, click, type, key, open, clipboard, applescript. Every tool prompts for permission.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"moxxy",
|
|
7
|
+
"agent",
|
|
8
|
+
"tools",
|
|
9
|
+
"computer-use",
|
|
10
|
+
"macos"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://moxxy.ai",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/moxxy-ai/moxxy/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/moxxy-ai/moxxy.git",
|
|
19
|
+
"directory": "packages/plugin-computer-control"
|
|
20
|
+
},
|
|
21
|
+
"author": "Michal Makowski <michal.makowski97@gmail.com>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./skills/*": "./skills/*"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"skills"
|
|
40
|
+
],
|
|
41
|
+
"moxxy": {
|
|
42
|
+
"plugin": {
|
|
43
|
+
"entry": "./dist/index.js",
|
|
44
|
+
"kind": "tools",
|
|
45
|
+
"skills": "./skills"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"zod": "^3.24.0",
|
|
50
|
+
"@moxxy/sdk": "0.26.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.10.0",
|
|
54
|
+
"typescript": "^5.7.3",
|
|
55
|
+
"vitest": "^2.1.8",
|
|
56
|
+
"@moxxy/tsconfig": "0.0.0",
|
|
57
|
+
"@moxxy/vitest-preset": "0.0.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -p tsconfig.json",
|
|
61
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"clean": "rm -rf dist .turbo"
|
|
64
|
+
}
|
|
65
|
+
}
|