junis 0.3.1 → 0.3.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/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.1",
34
+ version: "0.3.3",
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") return;
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
- "Execute terminal command",
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("Shell command to execute"),
386
- timeout_ms: import_zod.z.number().optional().default(3e4).describe("Timeout (ms)"),
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
- "Read file",
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("File path"),
422
- encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("Encoding")
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
- "Write/create file",
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("File content")
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
- "List directory contents",
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
- "Search code/text",
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 (regex supported)"),
476
- directory: import_zod.z.string().optional().default(".").describe("Search directory"),
477
- file_pattern: import_zod.z.string().optional().default("**/*").describe("File pattern")
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
- "Kill process (SIGTERM then 3s wait, auto SIGKILL if still alive)",
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 kill"),
528
- signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("Initial signal (default: SIGTERM). SIGKILL for immediate force kill)")
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
- "Replace a specific text block in a file with new text (diff-based partial edit)",
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("File path"),
577
- old_string: import_zod.z.string().describe("Existing text to replace (must match exactly)"),
578
- new_string: import_zod.z.string().describe("New text"),
579
- replace_all: import_zod.z.boolean().optional().default(false).describe("If true, replace all matches; if false, replace only the first")
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
- "Create a recurring cron job. schedule uses cron syntax (e.g. '0 9 * * 1-5' = weekdays 9am).",
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 (e.g. '*/5 * * * *' for every 5 min, '0 9 * * 1-5' for weekdays 9am)"),
617
- command: import_zod.z.string().describe("Shell command to execute"),
618
- label: import_zod.z.string().optional().describe("Optional label/comment for identification")
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 in the current user's crontab",
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 job matching this command string")
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
- "Start browser (BrowserClaw). mode='managed'(default) launches new Chromium; mode='remote-cdp' connects to existing Chrome via CDP URL.",
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 headless (managed mode only)"),
827
- cdpUrl: import_zod2.z.string().optional().describe("CDP URL for remote-cdp mode (e.g. http://localhost:9222)"),
828
- profile: import_zod2.z.string().optional().describe("Profile name (managed mode only)"),
829
- allowInternal: import_zod2.z.boolean().optional().default(false).describe("Allow localhost/internal URLs")
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. Opens new tab if browser started but no page yet.",
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
- "Get Accessibility Tree snapshot with ref numbers. Use refs to interact with elements (e.g. browser_click with ref='e1').",
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("Only include interactive elements"),
879
- compact: import_zod2.z.boolean().optional().default(true).describe("Remove empty containers")
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("Ref number from snapshot (e.g. 'e1')"),
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("Ref number from snapshot"),
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(s) by ref",
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 number from snapshot"),
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. 'Enter', 'Control+a', 'Escape')",
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 (e.g. 'Enter', 'Control+a', 'Escape', 'Tab')")
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
- "Hover mouse over element by ref",
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("Ref number from snapshot")
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(s) to file input element by ref",
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 number of file input element"),
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
- "Take screenshot of current page",
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 (if omitted, returns base64)"),
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("Save path (.pdf)")
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
- "Execute JavaScript in page context",
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 (wrap in function if needed)")
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
- "Wait for a condition: text appearance/disappearance, URL pattern, or fixed time",
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 (glob pattern, e.g. '**/dashboard')"),
1062
- loadState: import_zod2.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for load state"),
1063
- timeMs: import_zod2.z.number().optional().describe("Wait fixed milliseconds")
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
- "Get, set, or clear cookies",
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("Action to perform"),
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/write/clear localStorage or sessionStorage",
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("Action to perform"),
1110
- kind: import_zod2.z.enum(["local", "session"]).optional().default("local").describe("Storage type"),
1111
- key: import_zod2.z.string().optional().describe("Storage key (get/set)"),
1112
- value: import_zod2.z.string().optional().describe("Value to set (set action)")
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/confirm/prompt).",
1133
- "Two-step usage:",
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. Default: 30000."
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("Notebook file path") },
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
- "Edit a specific notebook cell",
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
- "Execute notebook (nbconvert --execute)",
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("Notebook file path"),
1241
- timeout: import_zod3.z.number().optional().default(300).describe("Timeout per cell (seconds)")
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
- "Add a new cell to notebook",
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 path"),
1273
- cell_type: import_zod3.z.enum(["code", "markdown"]).describe("Cell type"),
1274
- source: import_zod3.z.string().describe("Cell source content"),
1275
- position: import_zod3.z.number().optional().describe("Insert position (0-based). Appends to end if omitted")
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 specific notebook cell",
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 path"),
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
- "Camera photo capture",
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
- { text: import_zod4.z.string() },
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
- "Start/stop screen recording (macOS: screencapture -v, others: ffmpeg)",
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 on start, default: /tmp/junis_record_<timestamp>.mp4)")
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
- "Get current location (macOS: CoreLocation CLI, others: IP-based fallback)",
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 (macOS: afplay, others: ffplay)",
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("Path to the audio file to play")
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
- "Capture macOS Accessibility Tree snapshot. Returns structured element list with IDs for interaction. Use returned snapshotId in subsequent desktop_click/type calls for 240x speed improvement.",
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', 'Finder'). Omit for frontmost app.")
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
- "Click a UI element by label, accessibility ID, or coordinates",
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
- "Type text into the currently focused element",
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 target first")
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
- "Press keyboard shortcut (e.g. 'cmd,c' for copy, 'cmd,shift,t' for new tab)",
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 in an app or specific element",
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 (omit to query frontmost app)")
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
- "Take macOS screen screenshot using Peekaboo (Retina support, better quality than screen_capture)",
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("Capture mode")
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
- "Click menu bar item (e.g. 'File > New Tab')",
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', 'New Tab'])"),
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("data: ")) {
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
- return JSON.parse(line.slice(6));
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
- console.log(" Auto-starts on boot.");
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}`);