juno-cua 0.5.3
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/bin/setup.mjs +338 -0
- package/package.json +32 -0
- package/skill/SKILL.md +282 -0
package/bin/setup.mjs
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* juno-cua setup — configures AI coding agents to use Juno's desktop automation.
|
|
5
|
+
*
|
|
6
|
+
* Detects installed agents, checks for the juno-cua binary, and adds MCP config
|
|
7
|
+
* + skill files so agents can screenshot, click, type, and control your desktop.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx juno-cua # Interactive setup
|
|
11
|
+
* npx juno-cua --yes # Auto-approve all
|
|
12
|
+
* npx juno-cua --check # Just check what's installed, don't modify
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from "node:child_process";
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, symlinkSync, copyFileSync } from "node:fs";
|
|
17
|
+
import { createInterface } from "node:readline";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const HOME = homedir();
|
|
24
|
+
const AUTO_YES = process.argv.includes("--yes") || process.argv.includes("-y");
|
|
25
|
+
const CHECK_ONLY = process.argv.includes("--check");
|
|
26
|
+
const HELP = process.argv.includes("--help") || process.argv.includes("-h");
|
|
27
|
+
|
|
28
|
+
// ── Agent definitions ────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const AGENTS = [
|
|
31
|
+
{
|
|
32
|
+
name: "Claude Code",
|
|
33
|
+
id: "claude-code",
|
|
34
|
+
mcpConfig: join(HOME, ".claude", "mcp.json"),
|
|
35
|
+
skillDir: join(HOME, ".claude", "skills"),
|
|
36
|
+
detected: () => commandExists("claude"),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "Cursor",
|
|
40
|
+
id: "cursor",
|
|
41
|
+
mcpConfig: join(HOME, ".cursor", "mcp.json"),
|
|
42
|
+
skillDir: null,
|
|
43
|
+
detected: () => existsSync(join(HOME, ".cursor")),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "VS Code (Copilot)",
|
|
47
|
+
id: "vscode",
|
|
48
|
+
mcpConfig: join(HOME, ".vscode", "mcp.json"),
|
|
49
|
+
skillDir: null,
|
|
50
|
+
detected: () => existsSync(join(HOME, ".vscode")) || commandExists("code"),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "Windsurf",
|
|
54
|
+
id: "windsurf",
|
|
55
|
+
mcpConfig: join(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
56
|
+
skillDir: null,
|
|
57
|
+
detected: () => existsSync(join(HOME, ".codeium", "windsurf")),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "Codex",
|
|
61
|
+
id: "codex",
|
|
62
|
+
mcpConfig: join(HOME, ".codex", "mcp.json"),
|
|
63
|
+
skillDir: null,
|
|
64
|
+
detected: () => commandExists("codex"),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "Gemini CLI",
|
|
68
|
+
id: "gemini",
|
|
69
|
+
mcpConfig: join(HOME, ".gemini", "settings.json"),
|
|
70
|
+
skillDir: null,
|
|
71
|
+
detected: () => commandExists("gemini"),
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const MCP_ENTRY = {
|
|
76
|
+
command: "juno-cua",
|
|
77
|
+
args: ["serve-mcp"],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function commandExists(cmd) {
|
|
83
|
+
try {
|
|
84
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function log(msg) {
|
|
92
|
+
console.log(msg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function success(msg) {
|
|
96
|
+
console.log(` [ok] ${msg}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function warn(msg) {
|
|
100
|
+
console.log(` [!!] ${msg}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function info(msg) {
|
|
104
|
+
console.log(` [..] ${msg}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function skip(msg) {
|
|
108
|
+
console.log(` [--] ${msg}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function confirm(question) {
|
|
112
|
+
if (AUTO_YES) return true;
|
|
113
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
rl.question(` ${question} (y/N) `, (answer) => {
|
|
116
|
+
rl.close();
|
|
117
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readJson(path) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function writeJson(path, data) {
|
|
131
|
+
const dir = dirname(path);
|
|
132
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
133
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Binary check ─────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function checkBinary() {
|
|
139
|
+
log("\n--- juno-cua binary ---");
|
|
140
|
+
|
|
141
|
+
if (commandExists("juno-cua")) {
|
|
142
|
+
let version = "unknown";
|
|
143
|
+
try {
|
|
144
|
+
version = execSync("juno-cua --version", { encoding: "utf8" }).trim();
|
|
145
|
+
} catch { /* ignore */ }
|
|
146
|
+
success(`Found: ${version}`);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
warn("juno-cua not found on PATH");
|
|
151
|
+
log("");
|
|
152
|
+
log(" Install via Homebrew:");
|
|
153
|
+
log(" brew install lacymorrow/tap/juno-cua");
|
|
154
|
+
log("");
|
|
155
|
+
log(" Or build from source:");
|
|
156
|
+
log(" git clone https://github.com/lacymorrow/juno");
|
|
157
|
+
log(" cd juno && cargo build -p juno-cua --release");
|
|
158
|
+
log(" cp target/release/juno-cua /usr/local/bin/");
|
|
159
|
+
log("");
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── MCP config injection ─────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
function hasMcpEntry(config) {
|
|
166
|
+
const servers = config?.mcpServers || {};
|
|
167
|
+
return "juno" in servers || "juno-cua" in servers;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function injectMcpEntry(config) {
|
|
171
|
+
if (!config) config = {};
|
|
172
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
173
|
+
config.mcpServers["juno"] = MCP_ENTRY;
|
|
174
|
+
return config;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function configureMcp(agent) {
|
|
178
|
+
const existing = readJson(agent.mcpConfig);
|
|
179
|
+
|
|
180
|
+
if (existing && hasMcpEntry(existing)) {
|
|
181
|
+
skip(`${agent.name}: MCP already configured`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (CHECK_ONLY) {
|
|
186
|
+
info(`${agent.name}: MCP not configured (would add)`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const yes = await confirm(`Add juno MCP server to ${agent.name}?`);
|
|
191
|
+
if (!yes) {
|
|
192
|
+
skip(`${agent.name}: skipped`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// For Gemini, the MCP config lives under a different key
|
|
197
|
+
if (agent.id === "gemini") {
|
|
198
|
+
let settings = existing || {};
|
|
199
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
200
|
+
settings.mcpServers["juno"] = MCP_ENTRY;
|
|
201
|
+
writeJson(agent.mcpConfig, settings);
|
|
202
|
+
} else {
|
|
203
|
+
const config = injectMcpEntry(existing);
|
|
204
|
+
writeJson(agent.mcpConfig, config);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
success(`${agent.name}: MCP configured at ${agent.mcpConfig}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Skill installation (Claude Code only) ────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async function installSkill(agent) {
|
|
213
|
+
if (!agent.skillDir) return;
|
|
214
|
+
|
|
215
|
+
const skillTarget = join(agent.skillDir, "juno");
|
|
216
|
+
const skillSource = resolve(join(__dirname, "..", "skill"));
|
|
217
|
+
|
|
218
|
+
if (existsSync(skillTarget)) {
|
|
219
|
+
skip(`${agent.name}: Skill already installed`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (CHECK_ONLY) {
|
|
224
|
+
info(`${agent.name}: Skill not installed (would add)`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const yes = await confirm(`Install juno skill for ${agent.name}?`);
|
|
229
|
+
if (!yes) {
|
|
230
|
+
skip(`${agent.name}: skill skipped`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!existsSync(agent.skillDir)) {
|
|
235
|
+
mkdirSync(agent.skillDir, { recursive: true });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Copy SKILL.md into the skill directory
|
|
239
|
+
mkdirSync(skillTarget, { recursive: true });
|
|
240
|
+
const srcSkill = join(skillSource, "SKILL.md");
|
|
241
|
+
const dstSkill = join(skillTarget, "SKILL.md");
|
|
242
|
+
|
|
243
|
+
if (existsSync(srcSkill)) {
|
|
244
|
+
copyFileSync(srcSkill, dstSkill);
|
|
245
|
+
success(`${agent.name}: Skill installed at ${skillTarget}`);
|
|
246
|
+
} else {
|
|
247
|
+
warn(`Skill source not found at ${srcSkill}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
async function main() {
|
|
254
|
+
if (HELP) {
|
|
255
|
+
log("juno-cua — Setup desktop automation for AI coding agents");
|
|
256
|
+
log("");
|
|
257
|
+
log("Usage:");
|
|
258
|
+
log(" npx juno-cua Interactive setup");
|
|
259
|
+
log(" npx juno-cua --yes Auto-approve all changes");
|
|
260
|
+
log(" npx juno-cua --check Check status without modifying anything");
|
|
261
|
+
log(" npx juno-cua --help Show this help");
|
|
262
|
+
log("");
|
|
263
|
+
log("What it does:");
|
|
264
|
+
log(" 1. Checks for juno-cua binary on PATH");
|
|
265
|
+
log(" 2. Detects installed AI coding agents");
|
|
266
|
+
log(" 3. Adds MCP server config so agents can use desktop automation");
|
|
267
|
+
log(" 4. Installs the juno skill (Claude Code only)");
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
log("juno-cua setup — Desktop automation for AI agents");
|
|
272
|
+
log("==================================================");
|
|
273
|
+
|
|
274
|
+
// 1. Check binary
|
|
275
|
+
const hasBinary = checkBinary();
|
|
276
|
+
|
|
277
|
+
// 2. Detect agents
|
|
278
|
+
log("\n--- Detected agents ---");
|
|
279
|
+
const detected = AGENTS.filter((a) => a.detected());
|
|
280
|
+
|
|
281
|
+
if (detected.length === 0) {
|
|
282
|
+
warn("No supported agents detected");
|
|
283
|
+
log(" Supported: Claude Code, Cursor, VS Code, Windsurf, Codex, Gemini CLI");
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const agent of detected) {
|
|
288
|
+
success(agent.name);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const notDetected = AGENTS.filter((a) => !a.detected());
|
|
292
|
+
for (const agent of notDetected) {
|
|
293
|
+
skip(`${agent.name} (not found)`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!hasBinary && !CHECK_ONLY) {
|
|
297
|
+
warn("juno-cua binary not found — MCP server won't work until installed");
|
|
298
|
+
const proceed = await confirm("Continue with config setup anyway?");
|
|
299
|
+
if (!proceed) {
|
|
300
|
+
log("\nInstall juno-cua first, then re-run: npx juno-cua");
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 3. Configure MCP for detected agents
|
|
306
|
+
log("\n--- MCP configuration ---");
|
|
307
|
+
for (const agent of detected) {
|
|
308
|
+
await configureMcp(agent);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 4. Install skills (Claude Code only)
|
|
312
|
+
const skillAgents = detected.filter((a) => a.skillDir);
|
|
313
|
+
if (skillAgents.length > 0) {
|
|
314
|
+
log("\n--- Skill installation ---");
|
|
315
|
+
for (const agent of skillAgents) {
|
|
316
|
+
await installSkill(agent);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 5. Summary
|
|
321
|
+
log("\n--- Done ---");
|
|
322
|
+
if (hasBinary) {
|
|
323
|
+
log(" juno-cua is ready. Your agents can now:");
|
|
324
|
+
log(" - Take screenshots and analyze your screen");
|
|
325
|
+
log(" - Click, type, and scroll in any application");
|
|
326
|
+
log(" - Read accessibility trees for precise element targeting");
|
|
327
|
+
log(" - Open apps and URLs, manage the clipboard");
|
|
328
|
+
} else {
|
|
329
|
+
log(" Config written. Install the binary to activate:");
|
|
330
|
+
log(" brew install lacymorrow/tap/juno-cua");
|
|
331
|
+
}
|
|
332
|
+
log("");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
main().catch((err) => {
|
|
336
|
+
console.error(`Error: ${err.message}`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "juno-cua",
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Setup juno-cua desktop automation for AI coding agents (Claude Code, Cursor, Codex, Gemini CLI, etc.)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"juno",
|
|
7
|
+
"computer-use",
|
|
8
|
+
"desktop-automation",
|
|
9
|
+
"mcp",
|
|
10
|
+
"ai-agent",
|
|
11
|
+
"claude-code",
|
|
12
|
+
"cursor",
|
|
13
|
+
"codex"
|
|
14
|
+
],
|
|
15
|
+
"author": "Lacy Morrow <me@lacymorrow.com>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/lacymorrow/juno",
|
|
20
|
+
"directory": "packages/juno-cua"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"juno-cua": "./bin/setup.mjs"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"bin/",
|
|
27
|
+
"skill/"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# juno-cua — Desktop Automation for AI Agents
|
|
2
|
+
|
|
3
|
+
You have access to `juno-cua`, a CLI tool for macOS desktop automation. It lets you take screenshots, click, type, scroll, read accessibility trees, and control applications — all from the command line with JSON output.
|
|
4
|
+
|
|
5
|
+
## Quick Check
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
which juno-cua || echo "Not installed — run: brew install lacymorrow/tap/juno-cua"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
brew install lacymorrow/tap/juno-cua
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires macOS accessibility permissions (System Settings → Privacy & Security → Accessibility).
|
|
18
|
+
|
|
19
|
+
## When to Use juno-cua
|
|
20
|
+
|
|
21
|
+
**USE juno-cua when you need to:**
|
|
22
|
+
- See what's on screen (screenshot, UI tree)
|
|
23
|
+
- Click buttons, menus, or UI elements
|
|
24
|
+
- Type into GUI applications (not terminal)
|
|
25
|
+
- Fill forms in native apps
|
|
26
|
+
- Navigate between applications
|
|
27
|
+
- Automate repetitive GUI tasks
|
|
28
|
+
- Read or set the clipboard
|
|
29
|
+
|
|
30
|
+
**DON'T use juno-cua when:**
|
|
31
|
+
- You can accomplish the task with shell commands, file I/O, or APIs
|
|
32
|
+
- You're working in a terminal-only context with no GUI needed
|
|
33
|
+
- The task involves web scraping (use curl/fetch instead)
|
|
34
|
+
|
|
35
|
+
## Tool Reference
|
|
36
|
+
|
|
37
|
+
### Screenshot & Vision
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Take a screenshot — returns {"screenshot_base64": "..."}
|
|
41
|
+
juno-cua screenshot
|
|
42
|
+
|
|
43
|
+
# Get the accessibility tree (structured UI representation)
|
|
44
|
+
juno-cua ui-tree
|
|
45
|
+
juno-cua ui-tree --app "Safari"
|
|
46
|
+
|
|
47
|
+
# Find specific UI elements
|
|
48
|
+
juno-cua find-elements --selector "AXButton"
|
|
49
|
+
|
|
50
|
+
# Get info about the currently focused element
|
|
51
|
+
juno-cua focused-element
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Mouse
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Click at screen coordinates
|
|
58
|
+
juno-cua click --x 500 --y 300
|
|
59
|
+
juno-cua click --x 500 --y 300 --button right
|
|
60
|
+
juno-cua click --x 500 --y 300 --button double
|
|
61
|
+
|
|
62
|
+
# Move mouse without clicking
|
|
63
|
+
juno-cua mouse-move --x 500 --y 300
|
|
64
|
+
|
|
65
|
+
# Get current cursor position
|
|
66
|
+
juno-cua cursor-position
|
|
67
|
+
|
|
68
|
+
# Scroll at a position
|
|
69
|
+
juno-cua scroll --x 500 --y 300 --direction down
|
|
70
|
+
juno-cua scroll --x 500 --y 300 --direction down --amount 5
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Keyboard
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Type text (simulates keystrokes)
|
|
77
|
+
juno-cua type-text --text "Hello, world!"
|
|
78
|
+
|
|
79
|
+
# Press a key with optional modifier
|
|
80
|
+
juno-cua press-key --key Return
|
|
81
|
+
juno-cua press-key --key c --modifier cmd # Cmd+C (copy)
|
|
82
|
+
juno-cua press-key --key Tab
|
|
83
|
+
juno-cua press-key --key space
|
|
84
|
+
|
|
85
|
+
# Hold and release keys
|
|
86
|
+
juno-cua hold-key --key shift --duration-ms 500
|
|
87
|
+
juno-cua release-key --key shift
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### System
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Clipboard
|
|
94
|
+
juno-cua get-clipboard
|
|
95
|
+
juno-cua set-clipboard --content "copied text"
|
|
96
|
+
|
|
97
|
+
# Launch apps / open URLs
|
|
98
|
+
juno-cua open-app --name "Safari"
|
|
99
|
+
juno-cua open-url --url "https://example.com"
|
|
100
|
+
|
|
101
|
+
# Wait (useful between UI actions)
|
|
102
|
+
juno-cua wait --ms 1000
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Advanced
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# List all tools with full JSON schemas
|
|
109
|
+
juno-cua list-tools
|
|
110
|
+
|
|
111
|
+
# Generic tool call (for tools not exposed as subcommands)
|
|
112
|
+
juno-cua call --tool leftClick --args '{"x": 100, "y": 200}'
|
|
113
|
+
|
|
114
|
+
# Print capabilities catalog
|
|
115
|
+
juno-cua capabilities
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Output
|
|
119
|
+
|
|
120
|
+
All commands return JSON by default. Use `--format pretty` for human-readable output, or `--format quiet` for silent execution (exit code only).
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# JSON (default)
|
|
124
|
+
juno-cua cursor-position
|
|
125
|
+
# → {"x":512,"y":384}
|
|
126
|
+
|
|
127
|
+
# Pretty-printed
|
|
128
|
+
juno-cua cursor-position --format pretty
|
|
129
|
+
|
|
130
|
+
# Silent — just check exit code
|
|
131
|
+
juno-cua click --x 100 --y 200 --format quiet && echo "clicked"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Patterns
|
|
135
|
+
|
|
136
|
+
### Screenshot → Analyze → Act Loop
|
|
137
|
+
|
|
138
|
+
The most common pattern for GUI automation:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# 1. See what's on screen
|
|
142
|
+
juno-cua screenshot
|
|
143
|
+
# (analyze the base64 image to find UI elements)
|
|
144
|
+
|
|
145
|
+
# 2. Act on what you see
|
|
146
|
+
juno-cua click --x 340 --y 220
|
|
147
|
+
|
|
148
|
+
# 3. Wait for UI to update
|
|
149
|
+
juno-cua wait --ms 500
|
|
150
|
+
|
|
151
|
+
# 4. Verify the result
|
|
152
|
+
juno-cua screenshot
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Accessibility-First (Preferred)
|
|
156
|
+
|
|
157
|
+
When possible, use the accessibility tree instead of screenshots — it's faster and more reliable:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Get structured UI info
|
|
161
|
+
juno-cua ui-tree --app "System Settings"
|
|
162
|
+
|
|
163
|
+
# Find specific elements
|
|
164
|
+
juno-cua find-elements --selector "AXButton"
|
|
165
|
+
|
|
166
|
+
# Check focused element after an action
|
|
167
|
+
juno-cua focused-element
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Form Filling
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Click the first field
|
|
174
|
+
juno-cua click --x 300 --y 200
|
|
175
|
+
juno-cua wait --ms 200
|
|
176
|
+
|
|
177
|
+
# Type and tab to next field
|
|
178
|
+
juno-cua type-text --text "John Doe"
|
|
179
|
+
juno-cua press-key --key Tab
|
|
180
|
+
juno-cua type-text --text "john@example.com"
|
|
181
|
+
juno-cua press-key --key Tab
|
|
182
|
+
juno-cua type-text --text "password123"
|
|
183
|
+
|
|
184
|
+
# Submit
|
|
185
|
+
juno-cua press-key --key Return
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### App Navigation
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Open an app
|
|
192
|
+
juno-cua open-app --name "Finder"
|
|
193
|
+
juno-cua wait --ms 1000
|
|
194
|
+
|
|
195
|
+
# Use keyboard shortcuts
|
|
196
|
+
juno-cua press-key --key n --modifier cmd # New window
|
|
197
|
+
juno-cua press-key --key g --modifier cmd # Go to folder
|
|
198
|
+
juno-cua wait --ms 500
|
|
199
|
+
juno-cua type-text --text "/tmp"
|
|
200
|
+
juno-cua press-key --key Return
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Copy Text from GUI
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Select all and copy
|
|
207
|
+
juno-cua press-key --key a --modifier cmd
|
|
208
|
+
juno-cua press-key --key c --modifier cmd
|
|
209
|
+
juno-cua wait --ms 200
|
|
210
|
+
|
|
211
|
+
# Read clipboard
|
|
212
|
+
juno-cua get-clipboard
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Error Handling
|
|
216
|
+
|
|
217
|
+
All errors are returned as JSON on stderr with a non-zero exit code:
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{"error": "Screenshot failed: Check accessibility permissions."}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Common issues:
|
|
224
|
+
- **Accessibility permissions**: Grant in System Settings → Privacy & Security → Accessibility
|
|
225
|
+
- **App not found**: Check exact app name with `open-app`
|
|
226
|
+
- **Coordinates out of bounds**: Use `screenshot` first to find valid coordinates
|
|
227
|
+
|
|
228
|
+
## MCP Server (Alternative to CLI)
|
|
229
|
+
|
|
230
|
+
For agents that support MCP (Model Context Protocol), `juno-cua` can run as a structured tool server instead of being called via Bash. This gives typed tool schemas and native image content blocks for screenshots.
|
|
231
|
+
|
|
232
|
+
### Setup
|
|
233
|
+
|
|
234
|
+
Add to your `.mcp.json` (works in Claude Code, Cursor, Codex, Gemini CLI, etc.):
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"mcpServers": {
|
|
239
|
+
"juno": {
|
|
240
|
+
"command": "juno-cua",
|
|
241
|
+
"args": ["serve-mcp"]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The MCP server exposes the same tools as the CLI — screenshots return as MCP image content blocks automatically.
|
|
248
|
+
|
|
249
|
+
### When to use MCP vs CLI
|
|
250
|
+
|
|
251
|
+
- **MCP**: Best when your agent natively supports MCP tools. Gives typed schemas, streaming-friendly output, and image content blocks.
|
|
252
|
+
- **CLI (Bash)**: Works with any agent that can run shell commands. Zero config, universal compatibility.
|
|
253
|
+
|
|
254
|
+
Both use the same `juno-cua` binary and the same underlying `Desktop` engine.
|
|
255
|
+
|
|
256
|
+
## Full Juno Orchestrator
|
|
257
|
+
|
|
258
|
+
If Juno.app is installed, you also have access to the full multi-agent orchestrator:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# Natural language desktop automation (requires Juno.app)
|
|
262
|
+
juno query "Open Safari, navigate to github.com, and take a screenshot"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
This routes through Juno's hierarchical agent system (Desktop Agent, Browser Agent, File Agent) for complex multi-step tasks. Install from https://github.com/lacymorrow/juno/releases.
|
|
266
|
+
|
|
267
|
+
### Full Juno MCP Server
|
|
268
|
+
|
|
269
|
+
The full Juno binary can also run as an MCP server, exposing everything `juno-cua` has PLUS the `query` tool:
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"mcpServers": {
|
|
274
|
+
"juno": {
|
|
275
|
+
"command": "juno",
|
|
276
|
+
"args": ["mcp", "serve"]
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The `query` tool is the key difference — it delegates to the multi-agent orchestrator. No other MCP server offers a full AI orchestrator as a single tool call.
|