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 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.