junis 0.3.1 → 0.3.2
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/dist/cli/index.js +351 -155
- package/dist/server/mcp.js +330 -152
- package/dist/server/stdio.js +228 -126
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "junis",
|
|
34
|
-
version: "0.3.
|
|
34
|
+
version: "0.3.2",
|
|
35
35
|
description: "One-line device control for AI agents",
|
|
36
36
|
bin: {
|
|
37
37
|
junis: "dist/cli/index.js"
|
|
@@ -96,6 +96,7 @@ function saveConfig(config) {
|
|
|
96
96
|
try {
|
|
97
97
|
import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
98
98
|
import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
99
|
+
import_fs.default.chmodSync(CONFIG_FILE, 384);
|
|
99
100
|
} catch (err) {
|
|
100
101
|
console.error(`
|
|
101
102
|
\u274C Failed to save config file: ${err.message}`);
|
|
@@ -224,6 +225,7 @@ var RelayClient = class {
|
|
|
224
225
|
reconnectDelay = 1e3;
|
|
225
226
|
heartbeatTimer = null;
|
|
226
227
|
destroyed = false;
|
|
228
|
+
lastPongTime = 0;
|
|
227
229
|
async connect() {
|
|
228
230
|
if (this.destroyed) return;
|
|
229
231
|
const url = `${JUNIS_WS}/ws/devices/${this.config.device_key}`;
|
|
@@ -236,13 +238,17 @@ var RelayClient = class {
|
|
|
236
238
|
if (this.ws !== ws) return;
|
|
237
239
|
console.log("\u2705 Connected to relay server");
|
|
238
240
|
this.reconnectDelay = 1e3;
|
|
241
|
+
this.lastPongTime = Date.now();
|
|
239
242
|
this.startHeartbeat();
|
|
240
243
|
});
|
|
241
244
|
ws.on("message", async (raw) => {
|
|
242
245
|
if (this.ws !== ws) return;
|
|
243
246
|
try {
|
|
244
247
|
const msg = JSON.parse(raw.toString());
|
|
245
|
-
if (msg.type === "pong")
|
|
248
|
+
if (msg.type === "pong") {
|
|
249
|
+
this.lastPongTime = Date.now();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
246
252
|
if (msg.type === "mcp_request") {
|
|
247
253
|
try {
|
|
248
254
|
const result = await this.onMCPRequest(msg.id, msg.payload);
|
|
@@ -295,6 +301,11 @@ var RelayClient = class {
|
|
|
295
301
|
}
|
|
296
302
|
startHeartbeat() {
|
|
297
303
|
this.heartbeatTimer = setInterval(() => {
|
|
304
|
+
if (Date.now() - this.lastPongTime > 9e4) {
|
|
305
|
+
console.warn("\u26A0\uFE0F Heartbeat timeout (90s no pong). Reconnecting...");
|
|
306
|
+
this.ws?.terminate();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
298
309
|
this.send({ type: "heartbeat" });
|
|
299
310
|
}, 3e4);
|
|
300
311
|
}
|
|
@@ -380,11 +391,26 @@ var FilesystemTools = class {
|
|
|
380
391
|
register(server) {
|
|
381
392
|
server.tool(
|
|
382
393
|
"execute_command",
|
|
383
|
-
|
|
394
|
+
[
|
|
395
|
+
"Execute a shell command on the user's local device.",
|
|
396
|
+
"",
|
|
397
|
+
"ROUTING:",
|
|
398
|
+
"- Use for system commands, package managers (npm, pip, brew), git, build tools, and scripting.",
|
|
399
|
+
"- For reading files prefer read_file, for editing prefer edit_block, for searching prefer search_code.",
|
|
400
|
+
"",
|
|
401
|
+
"BEHAVIOR:",
|
|
402
|
+
"- Safe, routine commands (ls, pwd, git status, echo): execute immediately without explanation.",
|
|
403
|
+
"- Destructive or irreversible commands (rm -rf, sudo, shutdown, mkfs): explain what will happen and get user confirmation first.",
|
|
404
|
+
"- If a command fails, analyze the error and suggest an alternative. Do not retry the identical command more than twice.",
|
|
405
|
+
"",
|
|
406
|
+
"SAFETY:",
|
|
407
|
+
"- Commands run with the user's full permissions. Never execute commands that could damage the system, expose credentials, or modify security settings without explicit user request.",
|
|
408
|
+
"- Avoid piping untrusted input into shells. Use absolute paths when possible. Quote paths containing spaces."
|
|
409
|
+
].join("\n"),
|
|
384
410
|
{
|
|
385
|
-
command: import_zod.z.string().describe("
|
|
386
|
-
timeout_ms: import_zod.z.number().optional().default(3e4).describe("
|
|
387
|
-
background: import_zod.z.boolean().optional().default(false).describe("Run in background")
|
|
411
|
+
command: import_zod.z.string().describe("The shell command to execute. Use absolute paths when possible. Quote paths containing spaces."),
|
|
412
|
+
timeout_ms: import_zod.z.number().optional().default(3e4).describe("Maximum execution time in milliseconds (default: 30000). Increase for long-running builds or downloads."),
|
|
413
|
+
background: import_zod.z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
|
|
388
414
|
},
|
|
389
415
|
async ({ command, timeout_ms, background }) => {
|
|
390
416
|
checkPermission("execute_command");
|
|
@@ -416,10 +442,15 @@ ${error.stderr ?? ""}`
|
|
|
416
442
|
);
|
|
417
443
|
server.tool(
|
|
418
444
|
"read_file",
|
|
419
|
-
|
|
445
|
+
[
|
|
446
|
+
"Read the contents of a file from the local filesystem.",
|
|
447
|
+
"",
|
|
448
|
+
"Returns file content as text (utf-8) or base64 for binary files. Supports any file type.",
|
|
449
|
+
"For searching within files, prefer search_code instead. For listing directory contents, use list_directory."
|
|
450
|
+
].join("\n"),
|
|
420
451
|
{
|
|
421
|
-
path: import_zod.z.string().describe("
|
|
422
|
-
encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("
|
|
452
|
+
path: import_zod.z.string().describe("Absolute or relative file path to read"),
|
|
453
|
+
encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("'utf-8' for text files (default), 'base64' for binary files (images, PDFs, archives)")
|
|
423
454
|
},
|
|
424
455
|
async ({ path: filePath, encoding }) => {
|
|
425
456
|
try {
|
|
@@ -436,10 +467,15 @@ ${error.stderr ?? ""}`
|
|
|
436
467
|
);
|
|
437
468
|
server.tool(
|
|
438
469
|
"write_file",
|
|
439
|
-
|
|
470
|
+
[
|
|
471
|
+
"Create a new file or completely overwrite an existing file. Parent directories are created automatically.",
|
|
472
|
+
"",
|
|
473
|
+
"WARNING: This replaces the entire file content. For partial modifications, use edit_block instead.",
|
|
474
|
+
"Prefer edit_block over write_file for existing files \u2014 it's safer and preserves unmodified content."
|
|
475
|
+
].join("\n"),
|
|
440
476
|
{
|
|
441
|
-
path: import_zod.z.string().describe("File path"),
|
|
442
|
-
content: import_zod.z.string().describe("
|
|
477
|
+
path: import_zod.z.string().describe("File path to create or overwrite. Parent directories are auto-created."),
|
|
478
|
+
content: import_zod.z.string().describe("Complete file content. This replaces the entire file.")
|
|
443
479
|
},
|
|
444
480
|
async ({ path: filePath, content }) => {
|
|
445
481
|
checkPermission("write_file");
|
|
@@ -450,9 +486,12 @@ ${error.stderr ?? ""}`
|
|
|
450
486
|
);
|
|
451
487
|
server.tool(
|
|
452
488
|
"list_directory",
|
|
453
|
-
|
|
489
|
+
[
|
|
490
|
+
"List files and subdirectories in the specified path. Returns entries with type indicators (\u{1F4C1} directory, \u{1F4C4} file).",
|
|
491
|
+
"Use this to explore project structure before reading or modifying files."
|
|
492
|
+
].join("\n"),
|
|
454
493
|
{
|
|
455
|
-
path: import_zod.z.string().describe("Directory path")
|
|
494
|
+
path: import_zod.z.string().describe("Directory path to list")
|
|
456
495
|
},
|
|
457
496
|
async ({ path: dirPath }) => {
|
|
458
497
|
try {
|
|
@@ -470,11 +509,16 @@ ${error.stderr ?? ""}`
|
|
|
470
509
|
);
|
|
471
510
|
server.tool(
|
|
472
511
|
"search_code",
|
|
473
|
-
|
|
512
|
+
[
|
|
513
|
+
"Search for text patterns across files using regex. Uses ripgrep for speed with glob fallback.",
|
|
514
|
+
"",
|
|
515
|
+
"Use this to find code definitions, function references, configuration values, or any text pattern.",
|
|
516
|
+
"Returns matching lines with file paths and line numbers for precise navigation."
|
|
517
|
+
].join("\n"),
|
|
474
518
|
{
|
|
475
|
-
pattern: import_zod.z.string().describe("Search pattern
|
|
476
|
-
directory: import_zod.z.string().optional().default(".").describe("
|
|
477
|
-
file_pattern: import_zod.z.string().optional().default("**/*").describe("
|
|
519
|
+
pattern: import_zod.z.string().describe("Search pattern with full regex support (e.g. 'function\\s+\\w+', 'import.*from', 'TODO')"),
|
|
520
|
+
directory: import_zod.z.string().optional().default(".").describe("Root directory to search from (default: current working directory)"),
|
|
521
|
+
file_pattern: import_zod.z.string().optional().default("**/*").describe("Glob pattern to filter files (e.g. '**/*.ts', '*.py', 'src/**/*.js')")
|
|
478
522
|
},
|
|
479
523
|
async ({ pattern, directory, file_pattern }) => {
|
|
480
524
|
try {
|
|
@@ -512,7 +556,7 @@ ${error.stderr ?? ""}`
|
|
|
512
556
|
);
|
|
513
557
|
server.tool(
|
|
514
558
|
"list_processes",
|
|
515
|
-
"List running processes",
|
|
559
|
+
"List the top 30 running processes sorted by CPU usage. Use this to identify resource-heavy processes, find PIDs for kill_process, or diagnose performance issues.",
|
|
516
560
|
{},
|
|
517
561
|
async () => {
|
|
518
562
|
const cmd = process.platform === "win32" ? "tasklist" : process.platform === "darwin" ? "ps aux | sort -rk 3 | head -30" : "ps aux --sort=-%cpu | head -30";
|
|
@@ -522,10 +566,14 @@ ${error.stderr ?? ""}`
|
|
|
522
566
|
);
|
|
523
567
|
server.tool(
|
|
524
568
|
"kill_process",
|
|
525
|
-
|
|
569
|
+
[
|
|
570
|
+
"Terminate a process by PID. Default: sends SIGTERM (graceful shutdown), waits 3 seconds, then auto-applies SIGKILL if still alive.",
|
|
571
|
+
"",
|
|
572
|
+
"SAFETY: Only kill processes the user explicitly identifies. Never kill system-critical processes (init, systemd, loginwindow, WindowServer) without explicit instruction."
|
|
573
|
+
].join("\n"),
|
|
526
574
|
{
|
|
527
|
-
pid: import_zod.z.number().describe("PID of the process to
|
|
528
|
-
signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("
|
|
575
|
+
pid: import_zod.z.number().describe("PID of the process to terminate (use list_processes to find PIDs)"),
|
|
576
|
+
signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("SIGTERM (default): graceful shutdown with 3s auto-SIGKILL fallback. SIGKILL: immediate force kill.")
|
|
529
577
|
},
|
|
530
578
|
async ({ pid, signal }) => {
|
|
531
579
|
const isWindows = process.platform === "win32";
|
|
@@ -571,12 +619,20 @@ ${error.stderr ?? ""}`
|
|
|
571
619
|
);
|
|
572
620
|
server.tool(
|
|
573
621
|
"edit_block",
|
|
574
|
-
|
|
622
|
+
[
|
|
623
|
+
"Replace a specific text block in a file with new text (diff-based partial edit).",
|
|
624
|
+
"",
|
|
625
|
+
"WORKFLOW: Always use read_file first to see current content, then use edit_block with the exact text to replace.",
|
|
626
|
+
"The old_string must match character-for-character including whitespace, indentation, and line breaks.",
|
|
627
|
+
"If multiple matches exist, include more surrounding context to make it unique, or set replace_all=true.",
|
|
628
|
+
"",
|
|
629
|
+
"Prefer this over write_file for modifying existing files \u2014 it only changes what you specify and preserves the rest."
|
|
630
|
+
].join("\n"),
|
|
575
631
|
{
|
|
576
|
-
path: import_zod.z.string().describe("
|
|
577
|
-
old_string: import_zod.z.string().describe("
|
|
578
|
-
new_string: import_zod.z.string().describe("
|
|
579
|
-
replace_all: import_zod.z.boolean().optional().default(false).describe("If true, replace
|
|
632
|
+
path: import_zod.z.string().describe("Path to the file to edit. The file must already exist."),
|
|
633
|
+
old_string: import_zod.z.string().describe("The exact text to find and replace. Must match character-for-character including whitespace and newlines. Include enough context for uniqueness."),
|
|
634
|
+
new_string: import_zod.z.string().describe("The replacement text. Use empty string to delete the matched text."),
|
|
635
|
+
replace_all: import_zod.z.boolean().optional().default(false).describe("If true, replace ALL matches. If false (default), require exactly one match (errors on ambiguous multiple matches).")
|
|
580
636
|
},
|
|
581
637
|
async ({ path: filePath, old_string, new_string, replace_all }) => {
|
|
582
638
|
const content = await import_promises.default.readFile(filePath, "utf-8");
|
|
@@ -611,11 +667,16 @@ ${error.stderr ?? ""}`
|
|
|
611
667
|
);
|
|
612
668
|
server.tool(
|
|
613
669
|
"cron_create",
|
|
614
|
-
|
|
670
|
+
[
|
|
671
|
+
"Create a recurring scheduled task (cron job) using standard cron syntax.",
|
|
672
|
+
"",
|
|
673
|
+
"Common schedules: '*/5 * * * *' (every 5 min), '0 9 * * 1-5' (weekdays 9am), '0 0 * * *' (daily midnight), '0 */2 * * *' (every 2 hours).",
|
|
674
|
+
"Duplicate commands are automatically detected and rejected. Use cron_list to see existing jobs."
|
|
675
|
+
].join("\n"),
|
|
615
676
|
{
|
|
616
|
-
schedule: import_zod.z.string().describe("Cron schedule expression (
|
|
617
|
-
command: import_zod.z.string().describe("Shell command to execute"),
|
|
618
|
-
label: import_zod.z.string().optional().describe("
|
|
677
|
+
schedule: import_zod.z.string().describe("Cron schedule expression (5 fields: minute hour day month weekday). Examples: '*/5 * * * *' (every 5 min), '0 9 * * 1-5' (weekdays 9am)"),
|
|
678
|
+
command: import_zod.z.string().describe("Shell command to execute on schedule"),
|
|
679
|
+
label: import_zod.z.string().optional().describe("Human-readable label for identification (e.g. 'daily-backup', 'log-cleanup')")
|
|
619
680
|
},
|
|
620
681
|
async ({ schedule, command, label }) => {
|
|
621
682
|
try {
|
|
@@ -657,7 +718,7 @@ ${error.stderr ?? ""}`
|
|
|
657
718
|
);
|
|
658
719
|
server.tool(
|
|
659
720
|
"cron_list",
|
|
660
|
-
"List all cron jobs
|
|
721
|
+
"List all scheduled cron jobs with their IDs, labels, schedules, and commands. Use the returned ID numbers with cron_delete to remove specific jobs.",
|
|
661
722
|
{},
|
|
662
723
|
async () => {
|
|
663
724
|
try {
|
|
@@ -704,10 +765,10 @@ ${error.stderr ?? ""}`
|
|
|
704
765
|
);
|
|
705
766
|
server.tool(
|
|
706
767
|
"cron_delete",
|
|
707
|
-
"Delete a cron job by its ID (from cron_list) or by matching command string",
|
|
768
|
+
"Delete a scheduled cron job by its ID (from cron_list output) or by matching command string. Associated comment labels are automatically cleaned up.",
|
|
708
769
|
{
|
|
709
|
-
id: import_zod.z.number().optional().describe("Cron job ID from cron_list output"),
|
|
710
|
-
command: import_zod.z.string().optional().describe("Delete
|
|
770
|
+
id: import_zod.z.number().optional().describe("Cron job ID from cron_list output (e.g. 1, 2, 3)"),
|
|
771
|
+
command: import_zod.z.string().optional().describe("Delete all jobs matching this command string")
|
|
711
772
|
},
|
|
712
773
|
async ({ id, command }) => {
|
|
713
774
|
if (!id && !command) {
|
|
@@ -820,13 +881,22 @@ var BrowserTools = class {
|
|
|
820
881
|
};
|
|
821
882
|
server.tool(
|
|
822
883
|
"browser_start",
|
|
823
|
-
|
|
884
|
+
[
|
|
885
|
+
"Launch or connect to a web browser for automation.",
|
|
886
|
+
"",
|
|
887
|
+
"MODES:",
|
|
888
|
+
"- 'managed' (default): Launches a new Chromium instance. Use 'headless' for background operation, 'profile' for persistent sessions (cookies, logins preserved).",
|
|
889
|
+
"- 'remote-cdp': Connects to an already-running Chrome via CDP URL (e.g. from chrome://inspect). Use this to automate an existing browser session.",
|
|
890
|
+
"",
|
|
891
|
+
"WORKFLOW: browser_start \u2192 browser_navigate \u2192 browser_snapshot \u2192 interact (click/type/fill) \u2192 browser_stop.",
|
|
892
|
+
"Always call browser_stop when done to release system resources."
|
|
893
|
+
].join("\n"),
|
|
824
894
|
{
|
|
825
|
-
mode: import_zod2.z.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome"),
|
|
826
|
-
headless: import_zod2.z.boolean().optional().default(false).describe("Run
|
|
827
|
-
cdpUrl: import_zod2.z.string().optional().describe("
|
|
828
|
-
profile: import_zod2.z.string().optional().describe("
|
|
829
|
-
allowInternal: import_zod2.z.boolean().optional().default(false).describe("Allow localhost
|
|
895
|
+
mode: import_zod2.z.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome via CDP"),
|
|
896
|
+
headless: import_zod2.z.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
|
|
897
|
+
cdpUrl: import_zod2.z.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
|
|
898
|
+
profile: import_zod2.z.string().optional().describe("Browser profile name for persistent sessions \u2014 preserves cookies, logins, and history across restarts (managed mode only)"),
|
|
899
|
+
allowInternal: import_zod2.z.boolean().optional().default(false).describe("Allow navigation to localhost and internal network URLs")
|
|
830
900
|
},
|
|
831
901
|
({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
|
|
832
902
|
if (this.browser) {
|
|
@@ -847,7 +917,7 @@ var BrowserTools = class {
|
|
|
847
917
|
);
|
|
848
918
|
server.tool(
|
|
849
919
|
"browser_stop",
|
|
850
|
-
"Stop browser and release resources",
|
|
920
|
+
"Stop the browser and release all associated resources (memory, connections, processes). Always call this when browser automation is complete.",
|
|
851
921
|
{},
|
|
852
922
|
() => this.withLock(async () => {
|
|
853
923
|
await this.cleanup();
|
|
@@ -856,9 +926,9 @@ var BrowserTools = class {
|
|
|
856
926
|
);
|
|
857
927
|
server.tool(
|
|
858
928
|
"browser_navigate",
|
|
859
|
-
"Navigate to URL.
|
|
929
|
+
"Navigate the browser to a URL. Automatically opens a new tab if the browser is started but no page exists yet. Waits for the page to load before returning.",
|
|
860
930
|
{
|
|
861
|
-
url: import_zod2.z.string().describe("URL to navigate to")
|
|
931
|
+
url: import_zod2.z.string().describe("Full URL to navigate to (include https://)")
|
|
862
932
|
},
|
|
863
933
|
({ url }) => this.withLock(async () => {
|
|
864
934
|
if (!this.browser) throw new Error("Browser not started. Call browser_start first.");
|
|
@@ -873,10 +943,17 @@ var BrowserTools = class {
|
|
|
873
943
|
);
|
|
874
944
|
server.tool(
|
|
875
945
|
"browser_snapshot",
|
|
876
|
-
|
|
946
|
+
[
|
|
947
|
+
"Capture the page's Accessibility Tree with numbered ref IDs for each element. This is the primary way to 'see' and understand page content.",
|
|
948
|
+
"",
|
|
949
|
+
"WORKFLOW: Call browser_snapshot \u2192 find the target element's ref (e.g. 'e1', 'e5') \u2192 use that ref in browser_click, browser_type, or other interaction tools.",
|
|
950
|
+
"Refs change after page updates \u2014 always call browser_snapshot again after navigation or clicks that modify the page.",
|
|
951
|
+
"",
|
|
952
|
+
"Prefer this over browser_screenshot for understanding page structure \u2014 it's faster, structured, and machine-readable."
|
|
953
|
+
].join("\n"),
|
|
877
954
|
{
|
|
878
|
-
interactive: import_zod2.z.boolean().optional().default(true).describe("
|
|
879
|
-
compact: import_zod2.z.boolean().optional().default(true).describe("
|
|
955
|
+
interactive: import_zod2.z.boolean().optional().default(true).describe("true (default): only show clickable/typeable elements. false: show all elements including static text."),
|
|
956
|
+
compact: import_zod2.z.boolean().optional().default(true).describe("true (default): hide empty containers for cleaner output")
|
|
880
957
|
},
|
|
881
958
|
({ interactive, compact }) => this.withLock(async () => {
|
|
882
959
|
const result = await requirePage().snapshot({ interactive, compact });
|
|
@@ -896,11 +973,11 @@ ${refList}`
|
|
|
896
973
|
);
|
|
897
974
|
server.tool(
|
|
898
975
|
"browser_click",
|
|
899
|
-
"Click element by ref number from browser_snapshot",
|
|
976
|
+
"Click an element by its ref number from browser_snapshot. Always call browser_snapshot first to get current refs \u2014 they change after page updates.",
|
|
900
977
|
{
|
|
901
|
-
ref: import_zod2.z.string().describe("
|
|
902
|
-
doubleClick: import_zod2.z.boolean().optional().default(false),
|
|
903
|
-
button: import_zod2.z.enum(["left", "right", "middle"]).optional().default("left")
|
|
978
|
+
ref: import_zod2.z.string().describe("Element ref from browser_snapshot (e.g. 'e1', 'e15'). Call browser_snapshot first to get current refs."),
|
|
979
|
+
doubleClick: import_zod2.z.boolean().optional().default(false).describe("Double-click instead of single click"),
|
|
980
|
+
button: import_zod2.z.enum(["left", "right", "middle"]).optional().default("left").describe("Mouse button to use")
|
|
904
981
|
},
|
|
905
982
|
({ ref, doubleClick, button }) => this.withLock(async () => {
|
|
906
983
|
await requirePage().click(ref, { doubleClick, button });
|
|
@@ -909,12 +986,12 @@ ${refList}`
|
|
|
909
986
|
);
|
|
910
987
|
server.tool(
|
|
911
988
|
"browser_type",
|
|
912
|
-
"Type text into element by ref number",
|
|
989
|
+
"Type text into an input element by ref number. Use 'submit=true' to press Enter after typing (e.g. for search forms). Use 'slowly=true' for sites requiring keystroke-by-keystroke input.",
|
|
913
990
|
{
|
|
914
|
-
ref: import_zod2.z.string().describe("
|
|
915
|
-
text: import_zod2.z.string().describe("Text to type"),
|
|
916
|
-
submit: import_zod2.z.boolean().optional().default(false).describe("Press Enter after typing"),
|
|
917
|
-
slowly: import_zod2.z.boolean().optional().default(false).describe("Type slowly (75ms per char)")
|
|
991
|
+
ref: import_zod2.z.string().describe("Element ref from browser_snapshot (e.g. 'e3')"),
|
|
992
|
+
text: import_zod2.z.string().describe("Text to type into the element"),
|
|
993
|
+
submit: import_zod2.z.boolean().optional().default(false).describe("Press Enter after typing (useful for search boxes and forms)"),
|
|
994
|
+
slowly: import_zod2.z.boolean().optional().default(false).describe("Type slowly (75ms per char) for sites that process each keystroke")
|
|
918
995
|
},
|
|
919
996
|
({ ref, text, submit, slowly }) => this.withLock(async () => {
|
|
920
997
|
await requirePage().type(ref, text, { submit, slowly });
|
|
@@ -923,13 +1000,13 @@ ${refList}`
|
|
|
923
1000
|
);
|
|
924
1001
|
server.tool(
|
|
925
1002
|
"browser_fill",
|
|
926
|
-
"Fill multiple form fields at once",
|
|
1003
|
+
"Fill multiple form fields at once \u2014 more efficient than calling browser_type repeatedly. Each field needs a ref from browser_snapshot.",
|
|
927
1004
|
{
|
|
928
1005
|
fields: import_zod2.z.array(import_zod2.z.object({
|
|
929
1006
|
ref: import_zod2.z.string(),
|
|
930
1007
|
type: import_zod2.z.enum(["text", "checkbox", "radio"]),
|
|
931
1008
|
value: import_zod2.z.union([import_zod2.z.string(), import_zod2.z.boolean()])
|
|
932
|
-
})).describe("Array of {ref, type, value}")
|
|
1009
|
+
})).describe("Array of {ref, type, value}. type='text': value is string. type='checkbox'/'radio': value is boolean.")
|
|
933
1010
|
},
|
|
934
1011
|
({ fields }) => this.withLock(async () => {
|
|
935
1012
|
await requirePage().fill(fields);
|
|
@@ -938,9 +1015,9 @@ ${refList}`
|
|
|
938
1015
|
);
|
|
939
1016
|
server.tool(
|
|
940
1017
|
"browser_select",
|
|
941
|
-
"Select dropdown option
|
|
1018
|
+
"Select one or more options from a dropdown/select element. Values should match the option value attributes, not display text.",
|
|
942
1019
|
{
|
|
943
|
-
ref: import_zod2.z.string().describe("Ref
|
|
1020
|
+
ref: import_zod2.z.string().describe("Ref of the <select> element from browser_snapshot"),
|
|
944
1021
|
values: import_zod2.z.array(import_zod2.z.string()).describe("Option value(s) to select")
|
|
945
1022
|
},
|
|
946
1023
|
({ ref, values }) => this.withLock(async () => {
|
|
@@ -950,9 +1027,9 @@ ${refList}`
|
|
|
950
1027
|
);
|
|
951
1028
|
server.tool(
|
|
952
1029
|
"browser_press",
|
|
953
|
-
"Press keyboard key or combination (e.g. '
|
|
1030
|
+
"Press a keyboard key or key combination. Use for shortcuts (e.g. 'Control+a', 'Escape'), form submission ('Enter'), or navigation ('Tab'). Does not require a specific element ref.",
|
|
954
1031
|
{
|
|
955
|
-
key: import_zod2.z.string().describe("Key combination
|
|
1032
|
+
key: import_zod2.z.string().describe("Key or combination: 'Enter', 'Escape', 'Tab', 'Control+a', 'Meta+c', 'ArrowDown', 'Backspace'")
|
|
956
1033
|
},
|
|
957
1034
|
({ key }) => this.withLock(async () => {
|
|
958
1035
|
await requirePage().press(key);
|
|
@@ -961,9 +1038,9 @@ ${refList}`
|
|
|
961
1038
|
);
|
|
962
1039
|
server.tool(
|
|
963
1040
|
"browser_hover",
|
|
964
|
-
"
|
|
1041
|
+
"Move the mouse cursor over an element by ref. Use to trigger hover menus, tooltips, or dropdown previews before clicking.",
|
|
965
1042
|
{
|
|
966
|
-
ref: import_zod2.z.string().describe("
|
|
1043
|
+
ref: import_zod2.z.string().describe("Element ref from browser_snapshot")
|
|
967
1044
|
},
|
|
968
1045
|
({ ref }) => this.withLock(async () => {
|
|
969
1046
|
await requirePage().hover(ref);
|
|
@@ -972,10 +1049,10 @@ ${refList}`
|
|
|
972
1049
|
);
|
|
973
1050
|
server.tool(
|
|
974
1051
|
"browser_drag",
|
|
975
|
-
"Drag element from startRef to endRef",
|
|
1052
|
+
"Drag an element from startRef to endRef. Both refs must come from a recent browser_snapshot. Use for drag-and-drop interfaces, sliders, or reorderable lists.",
|
|
976
1053
|
{
|
|
977
|
-
startRef: import_zod2.z.string().describe("Source element ref"),
|
|
978
|
-
endRef: import_zod2.z.string().describe("Target element ref")
|
|
1054
|
+
startRef: import_zod2.z.string().describe("Source element ref to drag from"),
|
|
1055
|
+
endRef: import_zod2.z.string().describe("Target element ref to drag to")
|
|
979
1056
|
},
|
|
980
1057
|
({ startRef, endRef }) => this.withLock(async () => {
|
|
981
1058
|
await requirePage().drag(startRef, endRef);
|
|
@@ -984,10 +1061,10 @@ ${refList}`
|
|
|
984
1061
|
);
|
|
985
1062
|
server.tool(
|
|
986
1063
|
"browser_upload",
|
|
987
|
-
"Upload file(
|
|
1064
|
+
"Upload local files to a file input element (<input type='file'>). The ref must point to a file input from browser_snapshot.",
|
|
988
1065
|
{
|
|
989
|
-
ref: import_zod2.z.string().describe("Ref
|
|
990
|
-
paths: import_zod2.z.array(import_zod2.z.string()).describe("Absolute file path(s) to upload")
|
|
1066
|
+
ref: import_zod2.z.string().describe("Ref of the file input element from browser_snapshot"),
|
|
1067
|
+
paths: import_zod2.z.array(import_zod2.z.string()).describe("Absolute file path(s) on the local device to upload")
|
|
991
1068
|
},
|
|
992
1069
|
({ ref, paths }) => this.withLock(async () => {
|
|
993
1070
|
await requirePage().uploadFile(ref, paths);
|
|
@@ -996,11 +1073,16 @@ ${refList}`
|
|
|
996
1073
|
);
|
|
997
1074
|
server.tool(
|
|
998
1075
|
"browser_screenshot",
|
|
999
|
-
|
|
1076
|
+
[
|
|
1077
|
+
"Capture a screenshot of the current page. Returns base64 image data (viewable by AI) or saves to a file.",
|
|
1078
|
+
"",
|
|
1079
|
+
"Prefer browser_snapshot (Accessibility Tree) for understanding page structure \u2014 it's faster and machine-readable.",
|
|
1080
|
+
"Use browser_screenshot only when visual layout matters (charts, images, styling, visual verification)."
|
|
1081
|
+
].join("\n"),
|
|
1000
1082
|
{
|
|
1001
|
-
path: import_zod2.z.string().optional().describe("Save path
|
|
1002
|
-
fullPage: import_zod2.z.boolean().optional().default(false),
|
|
1003
|
-
ref: import_zod2.z.string().optional().describe("Capture specific element by ref")
|
|
1083
|
+
path: import_zod2.z.string().optional().describe("Save path for the screenshot. If omitted, returns base64 image data directly."),
|
|
1084
|
+
fullPage: import_zod2.z.boolean().optional().default(false).describe("Capture the full scrollable page, not just the visible viewport"),
|
|
1085
|
+
ref: import_zod2.z.string().optional().describe("Capture only a specific element by its ref from browser_snapshot")
|
|
1004
1086
|
},
|
|
1005
1087
|
({ path: path4, fullPage, ref }) => this.withLock(async () => {
|
|
1006
1088
|
const buffer = await requirePage().screenshot({ fullPage, ref });
|
|
@@ -1019,9 +1101,9 @@ ${refList}`
|
|
|
1019
1101
|
);
|
|
1020
1102
|
server.tool(
|
|
1021
1103
|
"browser_pdf",
|
|
1022
|
-
"Save current page as PDF",
|
|
1104
|
+
"Save the current page as a PDF file. Renders the full page including below-the-fold content. Useful for archiving, sharing, or offline reading.",
|
|
1023
1105
|
{
|
|
1024
|
-
path: import_zod2.z.string().describe("
|
|
1106
|
+
path: import_zod2.z.string().describe("Output file path (.pdf)")
|
|
1025
1107
|
},
|
|
1026
1108
|
({ path: path4 }) => this.withLock(async () => {
|
|
1027
1109
|
const buffer = await requirePage().pdf();
|
|
@@ -1031,9 +1113,14 @@ ${refList}`
|
|
|
1031
1113
|
);
|
|
1032
1114
|
server.tool(
|
|
1033
1115
|
"browser_evaluate",
|
|
1034
|
-
|
|
1116
|
+
[
|
|
1117
|
+
"Execute JavaScript code directly in the browser page context and return the result.",
|
|
1118
|
+
"",
|
|
1119
|
+
"Use for: extracting data not available in the Accessibility Tree, DOM manipulation, interacting with page APIs, or debugging.",
|
|
1120
|
+
"Wrap complex logic in an IIFE: (function(){ ... })()"
|
|
1121
|
+
].join("\n"),
|
|
1035
1122
|
{
|
|
1036
|
-
code: import_zod2.z.string().describe("JavaScript code to execute
|
|
1123
|
+
code: import_zod2.z.string().describe("JavaScript code to execute in the page context. Return values are automatically serialized.")
|
|
1037
1124
|
},
|
|
1038
1125
|
({ code }) => this.withLock(async () => {
|
|
1039
1126
|
try {
|
|
@@ -1054,13 +1141,17 @@ ${refList}`
|
|
|
1054
1141
|
);
|
|
1055
1142
|
server.tool(
|
|
1056
1143
|
"browser_wait",
|
|
1057
|
-
|
|
1144
|
+
[
|
|
1145
|
+
"Wait for a specific condition before proceeding. Use between actions when the page needs time to update.",
|
|
1146
|
+
"",
|
|
1147
|
+
"OPTIONS (use one): 'text' (wait for text to appear), 'textGone' (wait for text to disappear), 'url' (URL matches glob), 'loadState' (page load state), 'timeMs' (fixed delay as last resort)."
|
|
1148
|
+
].join("\n"),
|
|
1058
1149
|
{
|
|
1059
|
-
text: import_zod2.z.string().optional().describe("Wait until this text appears"),
|
|
1060
|
-
textGone: import_zod2.z.string().optional().describe("Wait until this text disappears"),
|
|
1061
|
-
url: import_zod2.z.string().optional().describe("Wait until URL matches
|
|
1062
|
-
loadState: import_zod2.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for load state"),
|
|
1063
|
-
timeMs: import_zod2.z.number().optional().describe("
|
|
1150
|
+
text: import_zod2.z.string().optional().describe("Wait until this text appears on the page"),
|
|
1151
|
+
textGone: import_zod2.z.string().optional().describe("Wait until this text disappears from the page"),
|
|
1152
|
+
url: import_zod2.z.string().optional().describe("Wait until URL matches this glob pattern (e.g. '**/dashboard', '**/success')"),
|
|
1153
|
+
loadState: import_zod2.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for page load state: 'load' (full), 'domcontentloaded' (DOM ready), 'networkidle' (no pending requests)"),
|
|
1154
|
+
timeMs: import_zod2.z.number().optional().describe("Fixed wait in milliseconds \u2014 use as last resort when other conditions don't apply")
|
|
1064
1155
|
},
|
|
1065
1156
|
({ text, textGone, url, loadState, timeMs }) => this.withLock(async () => {
|
|
1066
1157
|
const condition = {};
|
|
@@ -1075,9 +1166,9 @@ ${refList}`
|
|
|
1075
1166
|
);
|
|
1076
1167
|
server.tool(
|
|
1077
1168
|
"browser_cookies",
|
|
1078
|
-
"
|
|
1169
|
+
"Manage browser cookies: get all cookies, set a specific cookie, or clear all cookies. Useful for authentication state, session management, or testing.",
|
|
1079
1170
|
{
|
|
1080
|
-
action: import_zod2.z.enum(["get", "set", "clear"]).describe("
|
|
1171
|
+
action: import_zod2.z.enum(["get", "set", "clear"]).describe("'get': retrieve all cookies, 'set': add/update a cookie, 'clear': remove all cookies"),
|
|
1081
1172
|
cookie: import_zod2.z.object({
|
|
1082
1173
|
name: import_zod2.z.string(),
|
|
1083
1174
|
value: import_zod2.z.string(),
|
|
@@ -1085,7 +1176,7 @@ ${refList}`
|
|
|
1085
1176
|
path: import_zod2.z.string().optional(),
|
|
1086
1177
|
httpOnly: import_zod2.z.boolean().optional(),
|
|
1087
1178
|
secure: import_zod2.z.boolean().optional()
|
|
1088
|
-
}).optional().describe("Cookie data (required for set action)")
|
|
1179
|
+
}).optional().describe("Cookie data (required for 'set' action)")
|
|
1089
1180
|
},
|
|
1090
1181
|
({ action, cookie }) => this.withLock(async () => {
|
|
1091
1182
|
const page = requirePage();
|
|
@@ -1104,12 +1195,12 @@ ${refList}`
|
|
|
1104
1195
|
);
|
|
1105
1196
|
server.tool(
|
|
1106
1197
|
"browser_storage",
|
|
1107
|
-
"Read
|
|
1198
|
+
"Read, write, or clear browser localStorage/sessionStorage. Useful for managing client-side state, authentication tokens, or application preferences.",
|
|
1108
1199
|
{
|
|
1109
|
-
action: import_zod2.z.enum(["get", "set", "clear"]).describe("
|
|
1110
|
-
kind: import_zod2.z.enum(["local", "session"]).optional().default("local").describe("
|
|
1111
|
-
key: import_zod2.z.string().optional().describe("Storage key
|
|
1112
|
-
value: import_zod2.z.string().optional().describe("Value to
|
|
1200
|
+
action: import_zod2.z.enum(["get", "set", "clear"]).describe("'get': read value(s), 'set': write a key-value pair, 'clear': remove all entries"),
|
|
1201
|
+
kind: import_zod2.z.enum(["local", "session"]).optional().default("local").describe("'local' (persistent) or 'session' (cleared on tab close)"),
|
|
1202
|
+
key: import_zod2.z.string().optional().describe("Storage key to get or set. Omit key with 'get' to retrieve all entries."),
|
|
1203
|
+
value: import_zod2.z.string().optional().describe("Value to store (required for 'set' action)")
|
|
1113
1204
|
},
|
|
1114
1205
|
({ action, kind, key, value }) => this.withLock(async () => {
|
|
1115
1206
|
const page = requirePage();
|
|
@@ -1129,13 +1220,13 @@ ${refList}`
|
|
|
1129
1220
|
server.tool(
|
|
1130
1221
|
"browser_dialog",
|
|
1131
1222
|
[
|
|
1132
|
-
"Handle JavaScript dialogs (alert
|
|
1133
|
-
"
|
|
1134
|
-
" 1. action='arm' \u2014 register a one-shot handler (returns immediately, does NOT block).",
|
|
1223
|
+
"Handle JavaScript dialogs (alert, confirm, prompt). Two-step pattern:",
|
|
1224
|
+
" 1. action='arm' \u2014 register a one-shot handler (returns immediately, does NOT block).",
|
|
1135
1225
|
" 2. Trigger the dialog (e.g. browser_click on the button that calls confirm()).",
|
|
1136
1226
|
" 3. action='wait' \u2014 await the handler to confirm the dialog was handled.",
|
|
1227
|
+
"",
|
|
1137
1228
|
"The 'accept' and 'promptText' params are only used with action='arm'."
|
|
1138
|
-
].join("
|
|
1229
|
+
].join("\n"),
|
|
1139
1230
|
{
|
|
1140
1231
|
action: import_zod2.z.enum(["arm", "wait"]).describe(
|
|
1141
1232
|
"'arm' = register handler and return immediately; 'wait' = await the previously armed handler"
|
|
@@ -1147,7 +1238,7 @@ ${refList}`
|
|
|
1147
1238
|
"Text to enter if the dialog is a prompt. Only used with action='arm'."
|
|
1148
1239
|
),
|
|
1149
1240
|
timeoutMs: import_zod2.z.number().optional().describe(
|
|
1150
|
-
"Timeout in ms for 'wait' action
|
|
1241
|
+
"Timeout in ms for 'wait' action (default: 30000). Increase for slow-loading dialogs."
|
|
1151
1242
|
)
|
|
1152
1243
|
},
|
|
1153
1244
|
({ action, accept, promptText, timeoutMs }) => this.withLock(async () => {
|
|
@@ -1198,8 +1289,8 @@ var NotebookTools = class {
|
|
|
1198
1289
|
register(server) {
|
|
1199
1290
|
server.tool(
|
|
1200
1291
|
"notebook_read",
|
|
1201
|
-
"Read .ipynb notebook",
|
|
1202
|
-
{ path: import_zod3.z.string().describe("
|
|
1292
|
+
"Read a Jupyter notebook (.ipynb) and return all cells with their types (code/markdown), source content, and output counts. Use this to understand notebook structure before making edits.",
|
|
1293
|
+
{ path: import_zod3.z.string().describe("Path to the .ipynb notebook file") },
|
|
1203
1294
|
async ({ path: filePath }) => {
|
|
1204
1295
|
const nb = await readNotebook(filePath);
|
|
1205
1296
|
const cells = nb.cells.map((cell, i) => ({
|
|
@@ -1215,11 +1306,11 @@ var NotebookTools = class {
|
|
|
1215
1306
|
);
|
|
1216
1307
|
server.tool(
|
|
1217
1308
|
"notebook_edit_cell",
|
|
1218
|
-
"
|
|
1309
|
+
"Replace the source code of a specific cell in a Jupyter notebook. Use notebook_read first to identify the correct cell index (0-based). Existing outputs for the cell are preserved \u2014 use notebook_execute to re-run.",
|
|
1219
1310
|
{
|
|
1220
|
-
path: import_zod3.z.string(),
|
|
1221
|
-
cell_index: import_zod3.z.number().describe("Cell index (0-based)"),
|
|
1222
|
-
source: import_zod3.z.string().describe("New source code")
|
|
1311
|
+
path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
|
|
1312
|
+
cell_index: import_zod3.z.number().describe("Cell index to edit (0-based). Use notebook_read to find the right index."),
|
|
1313
|
+
source: import_zod3.z.string().describe("New source code/content for the cell (replaces entire cell content)")
|
|
1223
1314
|
},
|
|
1224
1315
|
async ({ path: filePath, cell_index, source }) => {
|
|
1225
1316
|
const nb = await readNotebook(filePath);
|
|
@@ -1235,10 +1326,15 @@ var NotebookTools = class {
|
|
|
1235
1326
|
);
|
|
1236
1327
|
server.tool(
|
|
1237
1328
|
"notebook_execute",
|
|
1238
|
-
|
|
1329
|
+
[
|
|
1330
|
+
"Execute all cells in a Jupyter notebook using nbconvert. Results are saved in-place \u2014 the notebook file is updated with execution outputs.",
|
|
1331
|
+
"",
|
|
1332
|
+
"Requires Jupyter to be installed (pip install jupyter). The timeout applies per cell, not for the entire notebook.",
|
|
1333
|
+
"If execution fails on a cell, the error is captured in the cell output and subsequent cells may not execute."
|
|
1334
|
+
].join("\n"),
|
|
1239
1335
|
{
|
|
1240
|
-
path: import_zod3.z.string().describe("
|
|
1241
|
-
timeout: import_zod3.z.number().optional().default(300).describe("
|
|
1336
|
+
path: import_zod3.z.string().describe("Path to the .ipynb notebook file to execute"),
|
|
1337
|
+
timeout: import_zod3.z.number().optional().default(300).describe("Maximum execution time per cell in seconds (default: 300). Increase for cells with heavy computation.")
|
|
1242
1338
|
},
|
|
1243
1339
|
async ({ path: filePath, timeout }) => {
|
|
1244
1340
|
const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
|
|
@@ -1267,12 +1363,12 @@ var NotebookTools = class {
|
|
|
1267
1363
|
);
|
|
1268
1364
|
server.tool(
|
|
1269
1365
|
"notebook_add_cell",
|
|
1270
|
-
"
|
|
1366
|
+
"Insert a new cell into a Jupyter notebook. If position is omitted, the cell is appended at the end. Use cell_type='code' for executable Python cells, 'markdown' for documentation/text cells.",
|
|
1271
1367
|
{
|
|
1272
|
-
path: import_zod3.z.string().describe(".ipynb file
|
|
1273
|
-
cell_type: import_zod3.z.enum(["code", "markdown"]).describe("
|
|
1274
|
-
source: import_zod3.z.string().describe("Cell source content"),
|
|
1275
|
-
position: import_zod3.z.number().optional().describe("Insert position (0-based).
|
|
1368
|
+
path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
|
|
1369
|
+
cell_type: import_zod3.z.enum(["code", "markdown"]).describe("'code' for executable cells, 'markdown' for text/documentation cells"),
|
|
1370
|
+
source: import_zod3.z.string().describe("Cell source content (Python code or Markdown text)"),
|
|
1371
|
+
position: import_zod3.z.number().optional().describe("Insert position (0-based index). Omit to append at the end. If position exceeds cell count, appends at end with a warning.")
|
|
1276
1372
|
},
|
|
1277
1373
|
async ({ path: filePath, cell_type: cellType, source, position }) => {
|
|
1278
1374
|
const nb = await readNotebook(filePath);
|
|
@@ -1303,10 +1399,10 @@ var NotebookTools = class {
|
|
|
1303
1399
|
);
|
|
1304
1400
|
server.tool(
|
|
1305
1401
|
"notebook_delete_cell",
|
|
1306
|
-
"Delete a
|
|
1402
|
+
"Delete a cell from a Jupyter notebook by its 0-based index. Use notebook_read first to verify the cell content before deletion. This action cannot be undone.",
|
|
1307
1403
|
{
|
|
1308
|
-
path: import_zod3.z.string().describe(".ipynb file
|
|
1309
|
-
cell_index: import_zod3.z.number().describe("Cell index to delete (0-based)")
|
|
1404
|
+
path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
|
|
1405
|
+
cell_index: import_zod3.z.number().describe("Cell index to delete (0-based). Use notebook_read first to verify content.")
|
|
1310
1406
|
},
|
|
1311
1407
|
async ({ path: filePath, cell_index }) => {
|
|
1312
1408
|
const nb = await readNotebook(filePath);
|
|
@@ -1337,9 +1433,14 @@ var DeviceTools = class {
|
|
|
1337
1433
|
register(server) {
|
|
1338
1434
|
server.tool(
|
|
1339
1435
|
"camera_capture",
|
|
1340
|
-
|
|
1436
|
+
[
|
|
1437
|
+
"Capture a photo from the device's camera and return it as base64 image data.",
|
|
1438
|
+
"",
|
|
1439
|
+
"Platform-specific: macOS (imagesnap), Windows (ffmpeg/dshow), Linux (fswebcam).",
|
|
1440
|
+
"Requires a connected camera with OS permissions granted. If output_path is provided, the file is also saved to disk."
|
|
1441
|
+
].join("\n"),
|
|
1341
1442
|
{
|
|
1342
|
-
output_path: import_zod4.z.string().optional()
|
|
1443
|
+
output_path: import_zod4.z.string().optional().describe("File path to save the captured photo. If omitted, returns image data only (temp file auto-cleaned).")
|
|
1343
1444
|
},
|
|
1344
1445
|
async ({ output_path }) => {
|
|
1345
1446
|
const p = platform();
|
|
@@ -1375,10 +1476,10 @@ Please check if a camera is connected.` }],
|
|
|
1375
1476
|
);
|
|
1376
1477
|
server.tool(
|
|
1377
1478
|
"notification_send",
|
|
1378
|
-
"Send OS notification",
|
|
1479
|
+
"Send a native OS notification (banner/toast) to the user's desktop. Use for task completion alerts, reminders, or important status updates. The notification appears even when the terminal is not focused.",
|
|
1379
1480
|
{
|
|
1380
|
-
title: import_zod4.z.string().describe("Notification title"),
|
|
1381
|
-
message: import_zod4.z.string().describe("Notification body")
|
|
1481
|
+
title: import_zod4.z.string().describe("Notification title (displayed prominently)"),
|
|
1482
|
+
message: import_zod4.z.string().describe("Notification body text")
|
|
1382
1483
|
},
|
|
1383
1484
|
async ({ title, message }) => {
|
|
1384
1485
|
try {
|
|
@@ -1402,7 +1503,7 @@ Please check if a camera is connected.` }],
|
|
|
1402
1503
|
);
|
|
1403
1504
|
server.tool(
|
|
1404
1505
|
"clipboard_read",
|
|
1405
|
-
"Read clipboard",
|
|
1506
|
+
"Read the current contents of the system clipboard (text). Use to access content the user has copied. Platform-specific: macOS (pbpaste), Windows (PowerShell), Linux (xclip).",
|
|
1406
1507
|
{},
|
|
1407
1508
|
async () => {
|
|
1408
1509
|
const p = platform();
|
|
@@ -1413,8 +1514,10 @@ Please check if a camera is connected.` }],
|
|
|
1413
1514
|
);
|
|
1414
1515
|
server.tool(
|
|
1415
1516
|
"clipboard_write",
|
|
1416
|
-
"Write to clipboard",
|
|
1417
|
-
{
|
|
1517
|
+
"Write text to the system clipboard, replacing its current contents. Use to prepare content for the user to paste elsewhere.",
|
|
1518
|
+
{
|
|
1519
|
+
text: import_zod4.z.string().describe("Text to copy to the clipboard")
|
|
1520
|
+
},
|
|
1418
1521
|
async ({ text }) => {
|
|
1419
1522
|
const p = platform();
|
|
1420
1523
|
const cmd = {
|
|
@@ -1428,10 +1531,15 @@ Please check if a camera is connected.` }],
|
|
|
1428
1531
|
);
|
|
1429
1532
|
server.tool(
|
|
1430
1533
|
"screen_record",
|
|
1431
|
-
|
|
1534
|
+
[
|
|
1535
|
+
"Start or stop screen recording. Captures the full screen as MP4 video.",
|
|
1536
|
+
"",
|
|
1537
|
+
"Use action='start' to begin, action='stop' to end and save. Only one recording can be active at a time.",
|
|
1538
|
+
"Platform-specific: macOS (screencapture -v), Windows/Linux (ffmpeg)."
|
|
1539
|
+
].join("\n"),
|
|
1432
1540
|
{
|
|
1433
|
-
action: import_zod4.z.enum(["start", "stop"]).describe("start: begin recording, stop: end recording"),
|
|
1434
|
-
output_path: import_zod4.z.string().optional().describe("Output path (used
|
|
1541
|
+
action: import_zod4.z.enum(["start", "stop"]).describe("'start': begin recording, 'stop': end recording and save the file"),
|
|
1542
|
+
output_path: import_zod4.z.string().optional().describe("Output file path (used with 'start'). Default: /tmp/junis_record_<timestamp>.mp4")
|
|
1435
1543
|
},
|
|
1436
1544
|
async ({ action, output_path }) => {
|
|
1437
1545
|
const p = platform();
|
|
@@ -1462,7 +1570,12 @@ Please check if a camera is connected.` }],
|
|
|
1462
1570
|
);
|
|
1463
1571
|
server.tool(
|
|
1464
1572
|
"location_get",
|
|
1465
|
-
|
|
1573
|
+
[
|
|
1574
|
+
"Get the device's current geographic location.",
|
|
1575
|
+
"",
|
|
1576
|
+
"macOS: Uses CoreLocation (GPS-accurate) with IP-based fallback. Other platforms: IP-based geolocation (city-level accuracy only).",
|
|
1577
|
+
"Returns latitude, longitude, and (when available) city and country."
|
|
1578
|
+
].join("\n"),
|
|
1466
1579
|
{},
|
|
1467
1580
|
async () => {
|
|
1468
1581
|
const p = platform();
|
|
@@ -1489,9 +1602,9 @@ Please check if a camera is connected.` }],
|
|
|
1489
1602
|
);
|
|
1490
1603
|
server.tool(
|
|
1491
1604
|
"audio_play",
|
|
1492
|
-
"Play audio file
|
|
1605
|
+
"Play an audio file through the device's speakers. Supports MP3, WAV, AAC, and other common formats. Playback is synchronous \u2014 the tool returns after playback completes. Platform-specific: macOS (afplay), Windows/Linux (ffplay).",
|
|
1493
1606
|
{
|
|
1494
|
-
file_path: import_zod4.z.string().describe("
|
|
1607
|
+
file_path: import_zod4.z.string().describe("Absolute path to the audio file to play")
|
|
1495
1608
|
},
|
|
1496
1609
|
async ({ file_path }) => {
|
|
1497
1610
|
const p = platform();
|
|
@@ -1569,9 +1682,16 @@ var DesktopTools = class {
|
|
|
1569
1682
|
register(server) {
|
|
1570
1683
|
server.tool(
|
|
1571
1684
|
"desktop_see",
|
|
1572
|
-
|
|
1685
|
+
[
|
|
1686
|
+
"Capture the macOS Accessibility Tree snapshot for a running application. Returns structured element list with IDs, roles, labels, and positions.",
|
|
1687
|
+
"",
|
|
1688
|
+
"WORKFLOW: Call desktop_see \u2192 find target element \u2192 use its ID in desktop_click or desktop_type.",
|
|
1689
|
+
"Pass the returned snapshotId to subsequent calls for 240x speed improvement (cached lookup vs. full re-scan).",
|
|
1690
|
+
"",
|
|
1691
|
+
"SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger an automatic safety stop."
|
|
1692
|
+
].join("\n"),
|
|
1573
1693
|
{
|
|
1574
|
-
app: import_zod5.z.string().optional().describe("App name to target (e.g. 'Safari', '
|
|
1694
|
+
app: import_zod5.z.string().optional().describe("App name to target (e.g. 'Safari', 'Notes', 'Google Chrome'). Omit for the frontmost app.")
|
|
1575
1695
|
},
|
|
1576
1696
|
async ({ app }) => {
|
|
1577
1697
|
checkBlacklist(app);
|
|
@@ -1596,12 +1716,19 @@ var DesktopTools = class {
|
|
|
1596
1716
|
);
|
|
1597
1717
|
server.tool(
|
|
1598
1718
|
"desktop_click",
|
|
1599
|
-
|
|
1719
|
+
[
|
|
1720
|
+
"Click a macOS UI element by its accessibility label, ID, or x,y coordinates.",
|
|
1721
|
+
"",
|
|
1722
|
+
"The 'on' parameter accepts: element label text (e.g. 'Save'), accessibility ID from desktop_see, or coordinates as 'x,y' string.",
|
|
1723
|
+
"For faster interaction, pass the snapshotId from a recent desktop_see call.",
|
|
1724
|
+
"",
|
|
1725
|
+
"SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger automatic safety stop."
|
|
1726
|
+
].join("\n"),
|
|
1600
1727
|
{
|
|
1601
|
-
on: import_zod5.z.string().describe("Element label, ID, or 'x,y' coordinates to click"),
|
|
1602
|
-
app: import_zod5.z.string().optional().describe("App name to target"),
|
|
1603
|
-
snapshot: import_zod5.z.string().optional().describe("snapshotId from desktop_see for cached interaction (faster)"),
|
|
1604
|
-
doubleClick: import_zod5.z.boolean().optional().default(false).describe("Double-click")
|
|
1728
|
+
on: import_zod5.z.string().describe("Element label, accessibility ID, or 'x,y' coordinates to click"),
|
|
1729
|
+
app: import_zod5.z.string().optional().describe("App name to target (e.g. 'Safari')"),
|
|
1730
|
+
snapshot: import_zod5.z.string().optional().describe("snapshotId from desktop_see for cached interaction (240x faster)"),
|
|
1731
|
+
doubleClick: import_zod5.z.boolean().optional().default(false).describe("Double-click instead of single click")
|
|
1605
1732
|
},
|
|
1606
1733
|
async ({ on, app, snapshot, doubleClick }) => {
|
|
1607
1734
|
checkBlacklist(app);
|
|
@@ -1617,10 +1744,14 @@ var DesktopTools = class {
|
|
|
1617
1744
|
);
|
|
1618
1745
|
server.tool(
|
|
1619
1746
|
"desktop_type",
|
|
1620
|
-
|
|
1747
|
+
[
|
|
1748
|
+
"Type text into the currently focused UI element on macOS. The text is sent as keyboard input character-by-character.",
|
|
1749
|
+
"",
|
|
1750
|
+
"SAFETY: Terminal, iTerm, and Finder are blocked. Use desktop_see first to verify the correct element is focused."
|
|
1751
|
+
].join("\n"),
|
|
1621
1752
|
{
|
|
1622
|
-
text: import_zod5.z.string().describe("Text to type"),
|
|
1623
|
-
app: import_zod5.z.string().optional().describe("App name to
|
|
1753
|
+
text: import_zod5.z.string().describe("Text to type into the focused element"),
|
|
1754
|
+
app: import_zod5.z.string().optional().describe("App name to focus before typing")
|
|
1624
1755
|
},
|
|
1625
1756
|
async ({ text, app }) => {
|
|
1626
1757
|
checkBlacklist(app);
|
|
@@ -1634,9 +1765,15 @@ var DesktopTools = class {
|
|
|
1634
1765
|
);
|
|
1635
1766
|
server.tool(
|
|
1636
1767
|
"desktop_hotkey",
|
|
1637
|
-
|
|
1768
|
+
[
|
|
1769
|
+
"Press a keyboard shortcut on macOS. Keys are comma-separated.",
|
|
1770
|
+
"",
|
|
1771
|
+
"Common shortcuts: 'cmd,c' (copy), 'cmd,v' (paste), 'cmd,z' (undo), 'cmd,s' (save), 'cmd,w' (close tab), 'cmd,q' (quit), 'cmd,shift,t' (reopen tab), 'cmd,tab' (switch app).",
|
|
1772
|
+
"",
|
|
1773
|
+
"SAFETY: Terminal, iTerm, and Finder are blocked."
|
|
1774
|
+
].join("\n"),
|
|
1638
1775
|
{
|
|
1639
|
-
keys: import_zod5.z.string().describe("Comma-separated key combination (e.g. 'cmd,c', 'cmd,shift,t', 'escape')"),
|
|
1776
|
+
keys: import_zod5.z.string().describe("Comma-separated key combination (e.g. 'cmd,c', 'cmd,shift,t', 'escape', 'cmd,option,i')"),
|
|
1640
1777
|
app: import_zod5.z.string().optional().describe("App name to target")
|
|
1641
1778
|
},
|
|
1642
1779
|
async ({ keys, app }) => {
|
|
@@ -1651,11 +1788,11 @@ var DesktopTools = class {
|
|
|
1651
1788
|
);
|
|
1652
1789
|
server.tool(
|
|
1653
1790
|
"desktop_scroll",
|
|
1654
|
-
"Scroll
|
|
1791
|
+
"Scroll within a macOS application or specific UI element. Use 'ticks' to control scroll distance (default: 3). Can target a specific element by label or ID with the 'on' parameter.",
|
|
1655
1792
|
{
|
|
1656
1793
|
direction: import_zod5.z.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
|
|
1657
|
-
ticks: import_zod5.z.number().optional().default(3).describe("Number of scroll ticks"),
|
|
1658
|
-
on: import_zod5.z.string().optional().describe("Element label or ID to scroll within"),
|
|
1794
|
+
ticks: import_zod5.z.number().optional().default(3).describe("Number of scroll ticks (default: 3). Higher = more scrolling."),
|
|
1795
|
+
on: import_zod5.z.string().optional().describe("Element label or ID to scroll within (from desktop_see). Omit to scroll the active area."),
|
|
1659
1796
|
app: import_zod5.z.string().optional().describe("App name to target")
|
|
1660
1797
|
},
|
|
1661
1798
|
async ({ direction, ticks, on, app }) => {
|
|
@@ -1671,7 +1808,7 @@ var DesktopTools = class {
|
|
|
1671
1808
|
);
|
|
1672
1809
|
server.tool(
|
|
1673
1810
|
"desktop_list_apps",
|
|
1674
|
-
"List all running applications on macOS",
|
|
1811
|
+
"List all currently running applications on macOS. Returns app names that can be used as the 'app' parameter in other desktop tools (desktop_see, desktop_click, desktop_type, etc.).",
|
|
1675
1812
|
{},
|
|
1676
1813
|
async () => {
|
|
1677
1814
|
try {
|
|
@@ -1687,9 +1824,9 @@ var DesktopTools = class {
|
|
|
1687
1824
|
);
|
|
1688
1825
|
server.tool(
|
|
1689
1826
|
"desktop_list_windows",
|
|
1690
|
-
"List all open windows on macOS",
|
|
1827
|
+
"List all open windows on macOS, optionally filtered by app name. If no app is specified, lists windows for the frontmost application. Useful for identifying which windows are available for automation.",
|
|
1691
1828
|
{
|
|
1692
|
-
app: import_zod5.z.string().optional().describe("Filter by app name
|
|
1829
|
+
app: import_zod5.z.string().optional().describe("Filter by app name. Omit to query the frontmost app.")
|
|
1693
1830
|
},
|
|
1694
1831
|
async ({ app }) => {
|
|
1695
1832
|
checkBlacklist(app);
|
|
@@ -1715,10 +1852,15 @@ var DesktopTools = class {
|
|
|
1715
1852
|
);
|
|
1716
1853
|
server.tool(
|
|
1717
1854
|
"desktop_screenshot",
|
|
1718
|
-
|
|
1855
|
+
[
|
|
1856
|
+
"Take a high-quality macOS screenshot using Peekaboo (Retina display support). Returns base64 image data.",
|
|
1857
|
+
"",
|
|
1858
|
+
"MODES: 'screen' captures the full display, 'window' captures a specific app window.",
|
|
1859
|
+
"Prefer desktop_see (Accessibility Tree) for understanding UI structure \u2014 use screenshot only when visual appearance matters (layouts, images, colors)."
|
|
1860
|
+
].join("\n"),
|
|
1719
1861
|
{
|
|
1720
|
-
app: import_zod5.z.string().optional().describe("Capture specific app window"),
|
|
1721
|
-
mode: import_zod5.z.enum(["screen", "window"]).optional().default("screen").describe("
|
|
1862
|
+
app: import_zod5.z.string().optional().describe("Capture a specific app's window (by name)"),
|
|
1863
|
+
mode: import_zod5.z.enum(["screen", "window"]).optional().default("screen").describe("'screen': full display capture, 'window': specific app window only")
|
|
1722
1864
|
},
|
|
1723
1865
|
async ({ app, mode }) => {
|
|
1724
1866
|
checkBlacklist(app);
|
|
@@ -1745,10 +1887,15 @@ var DesktopTools = class {
|
|
|
1745
1887
|
);
|
|
1746
1888
|
server.tool(
|
|
1747
1889
|
"desktop_menu",
|
|
1748
|
-
|
|
1890
|
+
[
|
|
1891
|
+
"Click a menu bar item in a macOS application. Navigate nested menus by adding path segments.",
|
|
1892
|
+
"",
|
|
1893
|
+
"Examples: ['File', 'New Tab'], ['Edit', 'Find', 'Find...'], ['View', 'Enter Full Screen'].",
|
|
1894
|
+
"The target app must be running and accessible."
|
|
1895
|
+
].join("\n"),
|
|
1749
1896
|
{
|
|
1750
|
-
path: import_zod5.z.array(import_zod5.z.string()).describe("Menu path as array (e.g. ['File', '
|
|
1751
|
-
app: import_zod5.z.string().optional().describe("App name to target")
|
|
1897
|
+
path: import_zod5.z.array(import_zod5.z.string()).describe("Menu path as array (e.g. ['File', 'Save'], ['Edit', 'Find', 'Find...'])"),
|
|
1898
|
+
app: import_zod5.z.string().optional().describe("App name to target. Omit for the frontmost app.")
|
|
1752
1899
|
},
|
|
1753
1900
|
async ({ path: path4, app }) => {
|
|
1754
1901
|
checkBlacklist(app);
|
|
@@ -2012,16 +2159,58 @@ async function handleMCPRequest(id, payload) {
|
|
|
2012
2159
|
if (!res.ok) {
|
|
2013
2160
|
throw new Error(`MCP request failed: ${res.status} ${res.statusText}`);
|
|
2014
2161
|
}
|
|
2162
|
+
if (res.status === 202) {
|
|
2163
|
+
return null;
|
|
2164
|
+
}
|
|
2165
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
2166
|
+
if (contentType.includes("application/json")) {
|
|
2167
|
+
return res.json();
|
|
2168
|
+
}
|
|
2015
2169
|
const text = await res.text();
|
|
2016
2170
|
const lines = text.split("\n");
|
|
2171
|
+
let currentEventType = null;
|
|
2172
|
+
const collectedResults = [];
|
|
2173
|
+
let lastError = null;
|
|
2017
2174
|
for (const line of lines) {
|
|
2018
|
-
if (line.startsWith("
|
|
2175
|
+
if (line.startsWith("event: ")) {
|
|
2176
|
+
currentEventType = line.slice(7).trim();
|
|
2177
|
+
} else if (line.startsWith("data: ")) {
|
|
2178
|
+
const rawData = line.slice(6).trim();
|
|
2179
|
+
if (rawData === "") {
|
|
2180
|
+
currentEventType = null;
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2019
2183
|
try {
|
|
2020
|
-
|
|
2184
|
+
const parsed = JSON.parse(rawData);
|
|
2185
|
+
if (currentEventType === "error") {
|
|
2186
|
+
lastError = parsed;
|
|
2187
|
+
} else if (currentEventType === "message" || currentEventType === null) {
|
|
2188
|
+
collectedResults.push(parsed);
|
|
2189
|
+
}
|
|
2021
2190
|
} catch {
|
|
2022
2191
|
}
|
|
2192
|
+
currentEventType = null;
|
|
2193
|
+
} else if (line === "") {
|
|
2194
|
+
currentEventType = null;
|
|
2023
2195
|
}
|
|
2024
2196
|
}
|
|
2197
|
+
if (collectedResults.length === 0 && lastError !== null) {
|
|
2198
|
+
throw new Error(
|
|
2199
|
+
`MCP error event: ${JSON.stringify(lastError)}`
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
if (collectedResults.length > 1 && payload !== null && typeof payload === "object" && "id" in payload) {
|
|
2203
|
+
const requestId = payload.id;
|
|
2204
|
+
const matched = collectedResults.find(
|
|
2205
|
+
(r) => r !== null && typeof r === "object" && "id" in r && r.id === requestId
|
|
2206
|
+
);
|
|
2207
|
+
if (matched !== void 0) {
|
|
2208
|
+
return matched;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (collectedResults.length > 0) {
|
|
2212
|
+
return collectedResults[collectedResults.length - 1];
|
|
2213
|
+
}
|
|
2025
2214
|
return null;
|
|
2026
2215
|
}
|
|
2027
2216
|
|
|
@@ -2335,8 +2524,10 @@ async function runBackground(config, port) {
|
|
|
2335
2524
|
console.log(" STEP 5 \xB7 Starting Background Service");
|
|
2336
2525
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2337
2526
|
const svc = new ServiceManager();
|
|
2527
|
+
let serviceInstalled = false;
|
|
2338
2528
|
try {
|
|
2339
2529
|
await svc.install();
|
|
2530
|
+
serviceInstalled = true;
|
|
2340
2531
|
console.log(" \u25C9 Service registered ........... \u2705");
|
|
2341
2532
|
console.log(" \u25C9 Auto-start on boot ........... \u2705");
|
|
2342
2533
|
} catch (e) {
|
|
@@ -2348,7 +2539,12 @@ async function runBackground(config, port) {
|
|
|
2348
2539
|
console.log("");
|
|
2349
2540
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2350
2541
|
console.log(" \u2705 ALL SET \u2014 Junis is running in the background.");
|
|
2351
|
-
|
|
2542
|
+
if (serviceInstalled) {
|
|
2543
|
+
console.log(" Auto-starts on boot.");
|
|
2544
|
+
} else {
|
|
2545
|
+
console.log(" \u26A0\uFE0F Auto-start on boot is NOT enabled (service registration failed).");
|
|
2546
|
+
console.log(" Run 'npx junis stop' and try 'npx junis' again to retry.");
|
|
2547
|
+
}
|
|
2352
2548
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2353
2549
|
console.log("");
|
|
2354
2550
|
console.log(` \u2192 ${webUrl}`);
|