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.
@@ -104,11 +104,26 @@ var FilesystemTools = class {
104
104
  register(server) {
105
105
  server.tool(
106
106
  "execute_command",
107
- "Execute terminal command",
107
+ [
108
+ "Execute a shell command on the user's local device.",
109
+ "",
110
+ "ROUTING:",
111
+ "- Use for system commands, package managers (npm, pip, brew), git, build tools, and scripting.",
112
+ "- For reading files prefer read_file, for editing prefer edit_block, for searching prefer search_code.",
113
+ "",
114
+ "BEHAVIOR:",
115
+ "- Safe, routine commands (ls, pwd, git status, echo): execute immediately without explanation.",
116
+ "- Destructive or irreversible commands (rm -rf, sudo, shutdown, mkfs): explain what will happen and get user confirmation first.",
117
+ "- If a command fails, analyze the error and suggest an alternative. Do not retry the identical command more than twice.",
118
+ "",
119
+ "SAFETY:",
120
+ "- 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.",
121
+ "- Avoid piping untrusted input into shells. Use absolute paths when possible. Quote paths containing spaces."
122
+ ].join("\n"),
108
123
  {
109
- command: import_zod.z.string().describe("Shell command to execute"),
110
- timeout_ms: import_zod.z.number().optional().default(3e4).describe("Timeout (ms)"),
111
- background: import_zod.z.boolean().optional().default(false).describe("Run in background")
124
+ command: import_zod.z.string().describe("The shell command to execute. Use absolute paths when possible. Quote paths containing spaces."),
125
+ timeout_ms: import_zod.z.number().optional().default(3e4).describe("Maximum execution time in milliseconds (default: 30000). Increase for long-running builds or downloads."),
126
+ background: import_zod.z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
112
127
  },
113
128
  async ({ command, timeout_ms, background }) => {
114
129
  checkPermission("execute_command");
@@ -140,10 +155,15 @@ ${error.stderr ?? ""}`
140
155
  );
141
156
  server.tool(
142
157
  "read_file",
143
- "Read file",
158
+ [
159
+ "Read the contents of a file from the local filesystem.",
160
+ "",
161
+ "Returns file content as text (utf-8) or base64 for binary files. Supports any file type.",
162
+ "For searching within files, prefer search_code instead. For listing directory contents, use list_directory."
163
+ ].join("\n"),
144
164
  {
145
- path: import_zod.z.string().describe("File path"),
146
- encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("Encoding")
165
+ path: import_zod.z.string().describe("Absolute or relative file path to read"),
166
+ 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)")
147
167
  },
148
168
  async ({ path: filePath, encoding }) => {
149
169
  try {
@@ -160,10 +180,15 @@ ${error.stderr ?? ""}`
160
180
  );
161
181
  server.tool(
162
182
  "write_file",
163
- "Write/create file",
183
+ [
184
+ "Create a new file or completely overwrite an existing file. Parent directories are created automatically.",
185
+ "",
186
+ "WARNING: This replaces the entire file content. For partial modifications, use edit_block instead.",
187
+ "Prefer edit_block over write_file for existing files \u2014 it's safer and preserves unmodified content."
188
+ ].join("\n"),
164
189
  {
165
- path: import_zod.z.string().describe("File path"),
166
- content: import_zod.z.string().describe("File content")
190
+ path: import_zod.z.string().describe("File path to create or overwrite. Parent directories are auto-created."),
191
+ content: import_zod.z.string().describe("Complete file content. This replaces the entire file.")
167
192
  },
168
193
  async ({ path: filePath, content }) => {
169
194
  checkPermission("write_file");
@@ -174,9 +199,12 @@ ${error.stderr ?? ""}`
174
199
  );
175
200
  server.tool(
176
201
  "list_directory",
177
- "List directory contents",
202
+ [
203
+ "List files and subdirectories in the specified path. Returns entries with type indicators (\u{1F4C1} directory, \u{1F4C4} file).",
204
+ "Use this to explore project structure before reading or modifying files."
205
+ ].join("\n"),
178
206
  {
179
- path: import_zod.z.string().describe("Directory path")
207
+ path: import_zod.z.string().describe("Directory path to list")
180
208
  },
181
209
  async ({ path: dirPath }) => {
182
210
  try {
@@ -194,11 +222,16 @@ ${error.stderr ?? ""}`
194
222
  );
195
223
  server.tool(
196
224
  "search_code",
197
- "Search code/text",
225
+ [
226
+ "Search for text patterns across files using regex. Uses ripgrep for speed with glob fallback.",
227
+ "",
228
+ "Use this to find code definitions, function references, configuration values, or any text pattern.",
229
+ "Returns matching lines with file paths and line numbers for precise navigation."
230
+ ].join("\n"),
198
231
  {
199
- pattern: import_zod.z.string().describe("Search pattern (regex supported)"),
200
- directory: import_zod.z.string().optional().default(".").describe("Search directory"),
201
- file_pattern: import_zod.z.string().optional().default("**/*").describe("File pattern")
232
+ pattern: import_zod.z.string().describe("Search pattern with full regex support (e.g. 'function\\s+\\w+', 'import.*from', 'TODO')"),
233
+ directory: import_zod.z.string().optional().default(".").describe("Root directory to search from (default: current working directory)"),
234
+ file_pattern: import_zod.z.string().optional().default("**/*").describe("Glob pattern to filter files (e.g. '**/*.ts', '*.py', 'src/**/*.js')")
202
235
  },
203
236
  async ({ pattern, directory, file_pattern }) => {
204
237
  try {
@@ -236,7 +269,7 @@ ${error.stderr ?? ""}`
236
269
  );
237
270
  server.tool(
238
271
  "list_processes",
239
- "List running processes",
272
+ "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.",
240
273
  {},
241
274
  async () => {
242
275
  const cmd = process.platform === "win32" ? "tasklist" : process.platform === "darwin" ? "ps aux | sort -rk 3 | head -30" : "ps aux --sort=-%cpu | head -30";
@@ -246,10 +279,14 @@ ${error.stderr ?? ""}`
246
279
  );
247
280
  server.tool(
248
281
  "kill_process",
249
- "Kill process (SIGTERM then 3s wait, auto SIGKILL if still alive)",
282
+ [
283
+ "Terminate a process by PID. Default: sends SIGTERM (graceful shutdown), waits 3 seconds, then auto-applies SIGKILL if still alive.",
284
+ "",
285
+ "SAFETY: Only kill processes the user explicitly identifies. Never kill system-critical processes (init, systemd, loginwindow, WindowServer) without explicit instruction."
286
+ ].join("\n"),
250
287
  {
251
- pid: import_zod.z.number().describe("PID of the process to kill"),
252
- signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("Initial signal (default: SIGTERM). SIGKILL for immediate force kill)")
288
+ pid: import_zod.z.number().describe("PID of the process to terminate (use list_processes to find PIDs)"),
289
+ signal: import_zod.z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("SIGTERM (default): graceful shutdown with 3s auto-SIGKILL fallback. SIGKILL: immediate force kill.")
253
290
  },
254
291
  async ({ pid, signal }) => {
255
292
  const isWindows = process.platform === "win32";
@@ -295,12 +332,20 @@ ${error.stderr ?? ""}`
295
332
  );
296
333
  server.tool(
297
334
  "edit_block",
298
- "Replace a specific text block in a file with new text (diff-based partial edit)",
335
+ [
336
+ "Replace a specific text block in a file with new text (diff-based partial edit).",
337
+ "",
338
+ "WORKFLOW: Always use read_file first to see current content, then use edit_block with the exact text to replace.",
339
+ "The old_string must match character-for-character including whitespace, indentation, and line breaks.",
340
+ "If multiple matches exist, include more surrounding context to make it unique, or set replace_all=true.",
341
+ "",
342
+ "Prefer this over write_file for modifying existing files \u2014 it only changes what you specify and preserves the rest."
343
+ ].join("\n"),
299
344
  {
300
- path: import_zod.z.string().describe("File path"),
301
- old_string: import_zod.z.string().describe("Existing text to replace (must match exactly)"),
302
- new_string: import_zod.z.string().describe("New text"),
303
- replace_all: import_zod.z.boolean().optional().default(false).describe("If true, replace all matches; if false, replace only the first")
345
+ path: import_zod.z.string().describe("Path to the file to edit. The file must already exist."),
346
+ 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."),
347
+ new_string: import_zod.z.string().describe("The replacement text. Use empty string to delete the matched text."),
348
+ 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).")
304
349
  },
305
350
  async ({ path: filePath, old_string, new_string, replace_all }) => {
306
351
  const content = await import_promises.default.readFile(filePath, "utf-8");
@@ -335,11 +380,16 @@ ${error.stderr ?? ""}`
335
380
  );
336
381
  server.tool(
337
382
  "cron_create",
338
- "Create a recurring cron job. schedule uses cron syntax (e.g. '0 9 * * 1-5' = weekdays 9am).",
383
+ [
384
+ "Create a recurring scheduled task (cron job) using standard cron syntax.",
385
+ "",
386
+ "Common schedules: '*/5 * * * *' (every 5 min), '0 9 * * 1-5' (weekdays 9am), '0 0 * * *' (daily midnight), '0 */2 * * *' (every 2 hours).",
387
+ "Duplicate commands are automatically detected and rejected. Use cron_list to see existing jobs."
388
+ ].join("\n"),
339
389
  {
340
- schedule: import_zod.z.string().describe("Cron schedule expression (e.g. '*/5 * * * *' for every 5 min, '0 9 * * 1-5' for weekdays 9am)"),
341
- command: import_zod.z.string().describe("Shell command to execute"),
342
- label: import_zod.z.string().optional().describe("Optional label/comment for identification")
390
+ 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)"),
391
+ command: import_zod.z.string().describe("Shell command to execute on schedule"),
392
+ label: import_zod.z.string().optional().describe("Human-readable label for identification (e.g. 'daily-backup', 'log-cleanup')")
343
393
  },
344
394
  async ({ schedule, command, label }) => {
345
395
  try {
@@ -381,7 +431,7 @@ ${error.stderr ?? ""}`
381
431
  );
382
432
  server.tool(
383
433
  "cron_list",
384
- "List all cron jobs in the current user's crontab",
434
+ "List all scheduled cron jobs with their IDs, labels, schedules, and commands. Use the returned ID numbers with cron_delete to remove specific jobs.",
385
435
  {},
386
436
  async () => {
387
437
  try {
@@ -428,10 +478,10 @@ ${error.stderr ?? ""}`
428
478
  );
429
479
  server.tool(
430
480
  "cron_delete",
431
- "Delete a cron job by its ID (from cron_list) or by matching command string",
481
+ "Delete a scheduled cron job by its ID (from cron_list output) or by matching command string. Associated comment labels are automatically cleaned up.",
432
482
  {
433
- id: import_zod.z.number().optional().describe("Cron job ID from cron_list output"),
434
- command: import_zod.z.string().optional().describe("Delete job matching this command string")
483
+ id: import_zod.z.number().optional().describe("Cron job ID from cron_list output (e.g. 1, 2, 3)"),
484
+ command: import_zod.z.string().optional().describe("Delete all jobs matching this command string")
435
485
  },
436
486
  async ({ id, command }) => {
437
487
  if (!id && !command) {
@@ -544,13 +594,22 @@ var BrowserTools = class {
544
594
  };
545
595
  server.tool(
546
596
  "browser_start",
547
- "Start browser (BrowserClaw). mode='managed'(default) launches new Chromium; mode='remote-cdp' connects to existing Chrome via CDP URL.",
597
+ [
598
+ "Launch or connect to a web browser for automation.",
599
+ "",
600
+ "MODES:",
601
+ "- 'managed' (default): Launches a new Chromium instance. Use 'headless' for background operation, 'profile' for persistent sessions (cookies, logins preserved).",
602
+ "- 'remote-cdp': Connects to an already-running Chrome via CDP URL (e.g. from chrome://inspect). Use this to automate an existing browser session.",
603
+ "",
604
+ "WORKFLOW: browser_start \u2192 browser_navigate \u2192 browser_snapshot \u2192 interact (click/type/fill) \u2192 browser_stop.",
605
+ "Always call browser_stop when done to release system resources."
606
+ ].join("\n"),
548
607
  {
549
- mode: import_zod2.z.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome"),
550
- headless: import_zod2.z.boolean().optional().default(false).describe("Run headless (managed mode only)"),
551
- cdpUrl: import_zod2.z.string().optional().describe("CDP URL for remote-cdp mode (e.g. http://localhost:9222)"),
552
- profile: import_zod2.z.string().optional().describe("Profile name (managed mode only)"),
553
- allowInternal: import_zod2.z.boolean().optional().default(false).describe("Allow localhost/internal URLs")
608
+ mode: import_zod2.z.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome via CDP"),
609
+ headless: import_zod2.z.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
610
+ cdpUrl: import_zod2.z.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
611
+ profile: import_zod2.z.string().optional().describe("Browser profile name for persistent sessions \u2014 preserves cookies, logins, and history across restarts (managed mode only)"),
612
+ allowInternal: import_zod2.z.boolean().optional().default(false).describe("Allow navigation to localhost and internal network URLs")
554
613
  },
555
614
  ({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
556
615
  if (this.browser) {
@@ -571,7 +630,7 @@ var BrowserTools = class {
571
630
  );
572
631
  server.tool(
573
632
  "browser_stop",
574
- "Stop browser and release resources",
633
+ "Stop the browser and release all associated resources (memory, connections, processes). Always call this when browser automation is complete.",
575
634
  {},
576
635
  () => this.withLock(async () => {
577
636
  await this.cleanup();
@@ -580,9 +639,9 @@ var BrowserTools = class {
580
639
  );
581
640
  server.tool(
582
641
  "browser_navigate",
583
- "Navigate to URL. Opens new tab if browser started but no page yet.",
642
+ "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.",
584
643
  {
585
- url: import_zod2.z.string().describe("URL to navigate to")
644
+ url: import_zod2.z.string().describe("Full URL to navigate to (include https://)")
586
645
  },
587
646
  ({ url }) => this.withLock(async () => {
588
647
  if (!this.browser) throw new Error("Browser not started. Call browser_start first.");
@@ -597,10 +656,17 @@ var BrowserTools = class {
597
656
  );
598
657
  server.tool(
599
658
  "browser_snapshot",
600
- "Get Accessibility Tree snapshot with ref numbers. Use refs to interact with elements (e.g. browser_click with ref='e1').",
659
+ [
660
+ "Capture the page's Accessibility Tree with numbered ref IDs for each element. This is the primary way to 'see' and understand page content.",
661
+ "",
662
+ "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.",
663
+ "Refs change after page updates \u2014 always call browser_snapshot again after navigation or clicks that modify the page.",
664
+ "",
665
+ "Prefer this over browser_screenshot for understanding page structure \u2014 it's faster, structured, and machine-readable."
666
+ ].join("\n"),
601
667
  {
602
- interactive: import_zod2.z.boolean().optional().default(true).describe("Only include interactive elements"),
603
- compact: import_zod2.z.boolean().optional().default(true).describe("Remove empty containers")
668
+ interactive: import_zod2.z.boolean().optional().default(true).describe("true (default): only show clickable/typeable elements. false: show all elements including static text."),
669
+ compact: import_zod2.z.boolean().optional().default(true).describe("true (default): hide empty containers for cleaner output")
604
670
  },
605
671
  ({ interactive, compact }) => this.withLock(async () => {
606
672
  const result = await requirePage().snapshot({ interactive, compact });
@@ -620,11 +686,11 @@ ${refList}`
620
686
  );
621
687
  server.tool(
622
688
  "browser_click",
623
- "Click element by ref number from browser_snapshot",
689
+ "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.",
624
690
  {
625
- ref: import_zod2.z.string().describe("Ref number from snapshot (e.g. 'e1')"),
626
- doubleClick: import_zod2.z.boolean().optional().default(false),
627
- button: import_zod2.z.enum(["left", "right", "middle"]).optional().default("left")
691
+ ref: import_zod2.z.string().describe("Element ref from browser_snapshot (e.g. 'e1', 'e15'). Call browser_snapshot first to get current refs."),
692
+ doubleClick: import_zod2.z.boolean().optional().default(false).describe("Double-click instead of single click"),
693
+ button: import_zod2.z.enum(["left", "right", "middle"]).optional().default("left").describe("Mouse button to use")
628
694
  },
629
695
  ({ ref, doubleClick, button }) => this.withLock(async () => {
630
696
  await requirePage().click(ref, { doubleClick, button });
@@ -633,12 +699,12 @@ ${refList}`
633
699
  );
634
700
  server.tool(
635
701
  "browser_type",
636
- "Type text into element by ref number",
702
+ "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.",
637
703
  {
638
- ref: import_zod2.z.string().describe("Ref number from snapshot"),
639
- text: import_zod2.z.string().describe("Text to type"),
640
- submit: import_zod2.z.boolean().optional().default(false).describe("Press Enter after typing"),
641
- slowly: import_zod2.z.boolean().optional().default(false).describe("Type slowly (75ms per char)")
704
+ ref: import_zod2.z.string().describe("Element ref from browser_snapshot (e.g. 'e3')"),
705
+ text: import_zod2.z.string().describe("Text to type into the element"),
706
+ submit: import_zod2.z.boolean().optional().default(false).describe("Press Enter after typing (useful for search boxes and forms)"),
707
+ slowly: import_zod2.z.boolean().optional().default(false).describe("Type slowly (75ms per char) for sites that process each keystroke")
642
708
  },
643
709
  ({ ref, text, submit, slowly }) => this.withLock(async () => {
644
710
  await requirePage().type(ref, text, { submit, slowly });
@@ -647,13 +713,13 @@ ${refList}`
647
713
  );
648
714
  server.tool(
649
715
  "browser_fill",
650
- "Fill multiple form fields at once",
716
+ "Fill multiple form fields at once \u2014 more efficient than calling browser_type repeatedly. Each field needs a ref from browser_snapshot.",
651
717
  {
652
718
  fields: import_zod2.z.array(import_zod2.z.object({
653
719
  ref: import_zod2.z.string(),
654
720
  type: import_zod2.z.enum(["text", "checkbox", "radio"]),
655
721
  value: import_zod2.z.union([import_zod2.z.string(), import_zod2.z.boolean()])
656
- })).describe("Array of {ref, type, value}")
722
+ })).describe("Array of {ref, type, value}. type='text': value is string. type='checkbox'/'radio': value is boolean.")
657
723
  },
658
724
  ({ fields }) => this.withLock(async () => {
659
725
  await requirePage().fill(fields);
@@ -662,9 +728,9 @@ ${refList}`
662
728
  );
663
729
  server.tool(
664
730
  "browser_select",
665
- "Select dropdown option(s) by ref",
731
+ "Select one or more options from a dropdown/select element. Values should match the option value attributes, not display text.",
666
732
  {
667
- ref: import_zod2.z.string().describe("Ref number from snapshot"),
733
+ ref: import_zod2.z.string().describe("Ref of the <select> element from browser_snapshot"),
668
734
  values: import_zod2.z.array(import_zod2.z.string()).describe("Option value(s) to select")
669
735
  },
670
736
  ({ ref, values }) => this.withLock(async () => {
@@ -674,9 +740,9 @@ ${refList}`
674
740
  );
675
741
  server.tool(
676
742
  "browser_press",
677
- "Press keyboard key or combination (e.g. 'Enter', 'Control+a', 'Escape')",
743
+ "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.",
678
744
  {
679
- key: import_zod2.z.string().describe("Key combination (e.g. 'Enter', 'Control+a', 'Escape', 'Tab')")
745
+ key: import_zod2.z.string().describe("Key or combination: 'Enter', 'Escape', 'Tab', 'Control+a', 'Meta+c', 'ArrowDown', 'Backspace'")
680
746
  },
681
747
  ({ key }) => this.withLock(async () => {
682
748
  await requirePage().press(key);
@@ -685,9 +751,9 @@ ${refList}`
685
751
  );
686
752
  server.tool(
687
753
  "browser_hover",
688
- "Hover mouse over element by ref",
754
+ "Move the mouse cursor over an element by ref. Use to trigger hover menus, tooltips, or dropdown previews before clicking.",
689
755
  {
690
- ref: import_zod2.z.string().describe("Ref number from snapshot")
756
+ ref: import_zod2.z.string().describe("Element ref from browser_snapshot")
691
757
  },
692
758
  ({ ref }) => this.withLock(async () => {
693
759
  await requirePage().hover(ref);
@@ -696,10 +762,10 @@ ${refList}`
696
762
  );
697
763
  server.tool(
698
764
  "browser_drag",
699
- "Drag element from startRef to endRef",
765
+ "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.",
700
766
  {
701
- startRef: import_zod2.z.string().describe("Source element ref"),
702
- endRef: import_zod2.z.string().describe("Target element ref")
767
+ startRef: import_zod2.z.string().describe("Source element ref to drag from"),
768
+ endRef: import_zod2.z.string().describe("Target element ref to drag to")
703
769
  },
704
770
  ({ startRef, endRef }) => this.withLock(async () => {
705
771
  await requirePage().drag(startRef, endRef);
@@ -708,10 +774,10 @@ ${refList}`
708
774
  );
709
775
  server.tool(
710
776
  "browser_upload",
711
- "Upload file(s) to file input element by ref",
777
+ "Upload local files to a file input element (<input type='file'>). The ref must point to a file input from browser_snapshot.",
712
778
  {
713
- ref: import_zod2.z.string().describe("Ref number of file input element"),
714
- paths: import_zod2.z.array(import_zod2.z.string()).describe("Absolute file path(s) to upload")
779
+ ref: import_zod2.z.string().describe("Ref of the file input element from browser_snapshot"),
780
+ paths: import_zod2.z.array(import_zod2.z.string()).describe("Absolute file path(s) on the local device to upload")
715
781
  },
716
782
  ({ ref, paths }) => this.withLock(async () => {
717
783
  await requirePage().uploadFile(ref, paths);
@@ -720,11 +786,16 @@ ${refList}`
720
786
  );
721
787
  server.tool(
722
788
  "browser_screenshot",
723
- "Take screenshot of current page",
789
+ [
790
+ "Capture a screenshot of the current page. Returns base64 image data (viewable by AI) or saves to a file.",
791
+ "",
792
+ "Prefer browser_snapshot (Accessibility Tree) for understanding page structure \u2014 it's faster and machine-readable.",
793
+ "Use browser_screenshot only when visual layout matters (charts, images, styling, visual verification)."
794
+ ].join("\n"),
724
795
  {
725
- path: import_zod2.z.string().optional().describe("Save path (if omitted, returns base64)"),
726
- fullPage: import_zod2.z.boolean().optional().default(false),
727
- ref: import_zod2.z.string().optional().describe("Capture specific element by ref")
796
+ path: import_zod2.z.string().optional().describe("Save path for the screenshot. If omitted, returns base64 image data directly."),
797
+ fullPage: import_zod2.z.boolean().optional().default(false).describe("Capture the full scrollable page, not just the visible viewport"),
798
+ ref: import_zod2.z.string().optional().describe("Capture only a specific element by its ref from browser_snapshot")
728
799
  },
729
800
  ({ path: path2, fullPage, ref }) => this.withLock(async () => {
730
801
  const buffer = await requirePage().screenshot({ fullPage, ref });
@@ -743,9 +814,9 @@ ${refList}`
743
814
  );
744
815
  server.tool(
745
816
  "browser_pdf",
746
- "Save current page as PDF",
817
+ "Save the current page as a PDF file. Renders the full page including below-the-fold content. Useful for archiving, sharing, or offline reading.",
747
818
  {
748
- path: import_zod2.z.string().describe("Save path (.pdf)")
819
+ path: import_zod2.z.string().describe("Output file path (.pdf)")
749
820
  },
750
821
  ({ path: path2 }) => this.withLock(async () => {
751
822
  const buffer = await requirePage().pdf();
@@ -755,9 +826,14 @@ ${refList}`
755
826
  );
756
827
  server.tool(
757
828
  "browser_evaluate",
758
- "Execute JavaScript in page context",
829
+ [
830
+ "Execute JavaScript code directly in the browser page context and return the result.",
831
+ "",
832
+ "Use for: extracting data not available in the Accessibility Tree, DOM manipulation, interacting with page APIs, or debugging.",
833
+ "Wrap complex logic in an IIFE: (function(){ ... })()"
834
+ ].join("\n"),
759
835
  {
760
- code: import_zod2.z.string().describe("JavaScript code to execute (wrap in function if needed)")
836
+ code: import_zod2.z.string().describe("JavaScript code to execute in the page context. Return values are automatically serialized.")
761
837
  },
762
838
  ({ code }) => this.withLock(async () => {
763
839
  try {
@@ -778,13 +854,17 @@ ${refList}`
778
854
  );
779
855
  server.tool(
780
856
  "browser_wait",
781
- "Wait for a condition: text appearance/disappearance, URL pattern, or fixed time",
857
+ [
858
+ "Wait for a specific condition before proceeding. Use between actions when the page needs time to update.",
859
+ "",
860
+ "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)."
861
+ ].join("\n"),
782
862
  {
783
- text: import_zod2.z.string().optional().describe("Wait until this text appears"),
784
- textGone: import_zod2.z.string().optional().describe("Wait until this text disappears"),
785
- url: import_zod2.z.string().optional().describe("Wait until URL matches (glob pattern, e.g. '**/dashboard')"),
786
- loadState: import_zod2.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for load state"),
787
- timeMs: import_zod2.z.number().optional().describe("Wait fixed milliseconds")
863
+ text: import_zod2.z.string().optional().describe("Wait until this text appears on the page"),
864
+ textGone: import_zod2.z.string().optional().describe("Wait until this text disappears from the page"),
865
+ url: import_zod2.z.string().optional().describe("Wait until URL matches this glob pattern (e.g. '**/dashboard', '**/success')"),
866
+ loadState: import_zod2.z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for page load state: 'load' (full), 'domcontentloaded' (DOM ready), 'networkidle' (no pending requests)"),
867
+ timeMs: import_zod2.z.number().optional().describe("Fixed wait in milliseconds \u2014 use as last resort when other conditions don't apply")
788
868
  },
789
869
  ({ text, textGone, url, loadState, timeMs }) => this.withLock(async () => {
790
870
  const condition = {};
@@ -799,9 +879,9 @@ ${refList}`
799
879
  );
800
880
  server.tool(
801
881
  "browser_cookies",
802
- "Get, set, or clear cookies",
882
+ "Manage browser cookies: get all cookies, set a specific cookie, or clear all cookies. Useful for authentication state, session management, or testing.",
803
883
  {
804
- action: import_zod2.z.enum(["get", "set", "clear"]).describe("Action to perform"),
884
+ action: import_zod2.z.enum(["get", "set", "clear"]).describe("'get': retrieve all cookies, 'set': add/update a cookie, 'clear': remove all cookies"),
805
885
  cookie: import_zod2.z.object({
806
886
  name: import_zod2.z.string(),
807
887
  value: import_zod2.z.string(),
@@ -809,7 +889,7 @@ ${refList}`
809
889
  path: import_zod2.z.string().optional(),
810
890
  httpOnly: import_zod2.z.boolean().optional(),
811
891
  secure: import_zod2.z.boolean().optional()
812
- }).optional().describe("Cookie data (required for set action)")
892
+ }).optional().describe("Cookie data (required for 'set' action)")
813
893
  },
814
894
  ({ action, cookie }) => this.withLock(async () => {
815
895
  const page = requirePage();
@@ -828,12 +908,12 @@ ${refList}`
828
908
  );
829
909
  server.tool(
830
910
  "browser_storage",
831
- "Read/write/clear localStorage or sessionStorage",
911
+ "Read, write, or clear browser localStorage/sessionStorage. Useful for managing client-side state, authentication tokens, or application preferences.",
832
912
  {
833
- action: import_zod2.z.enum(["get", "set", "clear"]).describe("Action to perform"),
834
- kind: import_zod2.z.enum(["local", "session"]).optional().default("local").describe("Storage type"),
835
- key: import_zod2.z.string().optional().describe("Storage key (get/set)"),
836
- value: import_zod2.z.string().optional().describe("Value to set (set action)")
913
+ action: import_zod2.z.enum(["get", "set", "clear"]).describe("'get': read value(s), 'set': write a key-value pair, 'clear': remove all entries"),
914
+ kind: import_zod2.z.enum(["local", "session"]).optional().default("local").describe("'local' (persistent) or 'session' (cleared on tab close)"),
915
+ key: import_zod2.z.string().optional().describe("Storage key to get or set. Omit key with 'get' to retrieve all entries."),
916
+ value: import_zod2.z.string().optional().describe("Value to store (required for 'set' action)")
837
917
  },
838
918
  ({ action, kind, key, value }) => this.withLock(async () => {
839
919
  const page = requirePage();
@@ -853,13 +933,13 @@ ${refList}`
853
933
  server.tool(
854
934
  "browser_dialog",
855
935
  [
856
- "Handle JavaScript dialogs (alert/confirm/prompt).",
857
- "Two-step usage:",
858
- " 1. action='arm' \u2014 register a one-shot handler (returns immediately, does NOT block).",
936
+ "Handle JavaScript dialogs (alert, confirm, prompt). Two-step pattern:",
937
+ " 1. action='arm' \u2014 register a one-shot handler (returns immediately, does NOT block).",
859
938
  " 2. Trigger the dialog (e.g. browser_click on the button that calls confirm()).",
860
939
  " 3. action='wait' \u2014 await the handler to confirm the dialog was handled.",
940
+ "",
861
941
  "The 'accept' and 'promptText' params are only used with action='arm'."
862
- ].join(" "),
942
+ ].join("\n"),
863
943
  {
864
944
  action: import_zod2.z.enum(["arm", "wait"]).describe(
865
945
  "'arm' = register handler and return immediately; 'wait' = await the previously armed handler"
@@ -871,7 +951,7 @@ ${refList}`
871
951
  "Text to enter if the dialog is a prompt. Only used with action='arm'."
872
952
  ),
873
953
  timeoutMs: import_zod2.z.number().optional().describe(
874
- "Timeout in ms for 'wait' action. Default: 30000."
954
+ "Timeout in ms for 'wait' action (default: 30000). Increase for slow-loading dialogs."
875
955
  )
876
956
  },
877
957
  ({ action, accept, promptText, timeoutMs }) => this.withLock(async () => {
@@ -922,8 +1002,8 @@ var NotebookTools = class {
922
1002
  register(server) {
923
1003
  server.tool(
924
1004
  "notebook_read",
925
- "Read .ipynb notebook",
926
- { path: import_zod3.z.string().describe("Notebook file path") },
1005
+ "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.",
1006
+ { path: import_zod3.z.string().describe("Path to the .ipynb notebook file") },
927
1007
  async ({ path: filePath }) => {
928
1008
  const nb = await readNotebook(filePath);
929
1009
  const cells = nb.cells.map((cell, i) => ({
@@ -939,11 +1019,11 @@ var NotebookTools = class {
939
1019
  );
940
1020
  server.tool(
941
1021
  "notebook_edit_cell",
942
- "Edit a specific notebook cell",
1022
+ "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.",
943
1023
  {
944
- path: import_zod3.z.string(),
945
- cell_index: import_zod3.z.number().describe("Cell index (0-based)"),
946
- source: import_zod3.z.string().describe("New source code")
1024
+ path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
1025
+ cell_index: import_zod3.z.number().describe("Cell index to edit (0-based). Use notebook_read to find the right index."),
1026
+ source: import_zod3.z.string().describe("New source code/content for the cell (replaces entire cell content)")
947
1027
  },
948
1028
  async ({ path: filePath, cell_index, source }) => {
949
1029
  const nb = await readNotebook(filePath);
@@ -959,10 +1039,15 @@ var NotebookTools = class {
959
1039
  );
960
1040
  server.tool(
961
1041
  "notebook_execute",
962
- "Execute notebook (nbconvert --execute)",
1042
+ [
1043
+ "Execute all cells in a Jupyter notebook using nbconvert. Results are saved in-place \u2014 the notebook file is updated with execution outputs.",
1044
+ "",
1045
+ "Requires Jupyter to be installed (pip install jupyter). The timeout applies per cell, not for the entire notebook.",
1046
+ "If execution fails on a cell, the error is captured in the cell output and subsequent cells may not execute."
1047
+ ].join("\n"),
963
1048
  {
964
- path: import_zod3.z.string().describe("Notebook file path"),
965
- timeout: import_zod3.z.number().optional().default(300).describe("Timeout per cell (seconds)")
1049
+ path: import_zod3.z.string().describe("Path to the .ipynb notebook file to execute"),
1050
+ timeout: import_zod3.z.number().optional().default(300).describe("Maximum execution time per cell in seconds (default: 300). Increase for cells with heavy computation.")
966
1051
  },
967
1052
  async ({ path: filePath, timeout }) => {
968
1053
  const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
@@ -991,12 +1076,12 @@ var NotebookTools = class {
991
1076
  );
992
1077
  server.tool(
993
1078
  "notebook_add_cell",
994
- "Add a new cell to notebook",
1079
+ "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.",
995
1080
  {
996
- path: import_zod3.z.string().describe(".ipynb file path"),
997
- cell_type: import_zod3.z.enum(["code", "markdown"]).describe("Cell type"),
998
- source: import_zod3.z.string().describe("Cell source content"),
999
- position: import_zod3.z.number().optional().describe("Insert position (0-based). Appends to end if omitted")
1081
+ path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
1082
+ cell_type: import_zod3.z.enum(["code", "markdown"]).describe("'code' for executable cells, 'markdown' for text/documentation cells"),
1083
+ source: import_zod3.z.string().describe("Cell source content (Python code or Markdown text)"),
1084
+ 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.")
1000
1085
  },
1001
1086
  async ({ path: filePath, cell_type: cellType, source, position }) => {
1002
1087
  const nb = await readNotebook(filePath);
@@ -1027,10 +1112,10 @@ var NotebookTools = class {
1027
1112
  );
1028
1113
  server.tool(
1029
1114
  "notebook_delete_cell",
1030
- "Delete a specific notebook cell",
1115
+ "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.",
1031
1116
  {
1032
- path: import_zod3.z.string().describe(".ipynb file path"),
1033
- cell_index: import_zod3.z.number().describe("Cell index to delete (0-based)")
1117
+ path: import_zod3.z.string().describe("Path to the .ipynb notebook file"),
1118
+ cell_index: import_zod3.z.number().describe("Cell index to delete (0-based). Use notebook_read first to verify content.")
1034
1119
  },
1035
1120
  async ({ path: filePath, cell_index }) => {
1036
1121
  const nb = await readNotebook(filePath);
@@ -1061,9 +1146,14 @@ var DeviceTools = class {
1061
1146
  register(server) {
1062
1147
  server.tool(
1063
1148
  "camera_capture",
1064
- "Camera photo capture",
1149
+ [
1150
+ "Capture a photo from the device's camera and return it as base64 image data.",
1151
+ "",
1152
+ "Platform-specific: macOS (imagesnap), Windows (ffmpeg/dshow), Linux (fswebcam).",
1153
+ "Requires a connected camera with OS permissions granted. If output_path is provided, the file is also saved to disk."
1154
+ ].join("\n"),
1065
1155
  {
1066
- output_path: import_zod4.z.string().optional()
1156
+ 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).")
1067
1157
  },
1068
1158
  async ({ output_path }) => {
1069
1159
  const p = platform();
@@ -1099,10 +1189,10 @@ Please check if a camera is connected.` }],
1099
1189
  );
1100
1190
  server.tool(
1101
1191
  "notification_send",
1102
- "Send OS notification",
1192
+ "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.",
1103
1193
  {
1104
- title: import_zod4.z.string().describe("Notification title"),
1105
- message: import_zod4.z.string().describe("Notification body")
1194
+ title: import_zod4.z.string().describe("Notification title (displayed prominently)"),
1195
+ message: import_zod4.z.string().describe("Notification body text")
1106
1196
  },
1107
1197
  async ({ title, message }) => {
1108
1198
  try {
@@ -1126,7 +1216,7 @@ Please check if a camera is connected.` }],
1126
1216
  );
1127
1217
  server.tool(
1128
1218
  "clipboard_read",
1129
- "Read clipboard",
1219
+ "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).",
1130
1220
  {},
1131
1221
  async () => {
1132
1222
  const p = platform();
@@ -1137,8 +1227,10 @@ Please check if a camera is connected.` }],
1137
1227
  );
1138
1228
  server.tool(
1139
1229
  "clipboard_write",
1140
- "Write to clipboard",
1141
- { text: import_zod4.z.string() },
1230
+ "Write text to the system clipboard, replacing its current contents. Use to prepare content for the user to paste elsewhere.",
1231
+ {
1232
+ text: import_zod4.z.string().describe("Text to copy to the clipboard")
1233
+ },
1142
1234
  async ({ text }) => {
1143
1235
  const p = platform();
1144
1236
  const cmd = {
@@ -1152,10 +1244,15 @@ Please check if a camera is connected.` }],
1152
1244
  );
1153
1245
  server.tool(
1154
1246
  "screen_record",
1155
- "Start/stop screen recording (macOS: screencapture -v, others: ffmpeg)",
1247
+ [
1248
+ "Start or stop screen recording. Captures the full screen as MP4 video.",
1249
+ "",
1250
+ "Use action='start' to begin, action='stop' to end and save. Only one recording can be active at a time.",
1251
+ "Platform-specific: macOS (screencapture -v), Windows/Linux (ffmpeg)."
1252
+ ].join("\n"),
1156
1253
  {
1157
- action: import_zod4.z.enum(["start", "stop"]).describe("start: begin recording, stop: end recording"),
1158
- output_path: import_zod4.z.string().optional().describe("Output path (used on start, default: /tmp/junis_record_<timestamp>.mp4)")
1254
+ action: import_zod4.z.enum(["start", "stop"]).describe("'start': begin recording, 'stop': end recording and save the file"),
1255
+ output_path: import_zod4.z.string().optional().describe("Output file path (used with 'start'). Default: /tmp/junis_record_<timestamp>.mp4")
1159
1256
  },
1160
1257
  async ({ action, output_path }) => {
1161
1258
  const p = platform();
@@ -1186,7 +1283,12 @@ Please check if a camera is connected.` }],
1186
1283
  );
1187
1284
  server.tool(
1188
1285
  "location_get",
1189
- "Get current location (macOS: CoreLocation CLI, others: IP-based fallback)",
1286
+ [
1287
+ "Get the device's current geographic location.",
1288
+ "",
1289
+ "macOS: Uses CoreLocation (GPS-accurate) with IP-based fallback. Other platforms: IP-based geolocation (city-level accuracy only).",
1290
+ "Returns latitude, longitude, and (when available) city and country."
1291
+ ].join("\n"),
1190
1292
  {},
1191
1293
  async () => {
1192
1294
  const p = platform();
@@ -1213,9 +1315,9 @@ Please check if a camera is connected.` }],
1213
1315
  );
1214
1316
  server.tool(
1215
1317
  "audio_play",
1216
- "Play audio file (macOS: afplay, others: ffplay)",
1318
+ "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).",
1217
1319
  {
1218
- file_path: import_zod4.z.string().describe("Path to the audio file to play")
1320
+ file_path: import_zod4.z.string().describe("Absolute path to the audio file to play")
1219
1321
  },
1220
1322
  async ({ file_path }) => {
1221
1323
  const p = platform();
@@ -1293,9 +1395,16 @@ var DesktopTools = class {
1293
1395
  register(server) {
1294
1396
  server.tool(
1295
1397
  "desktop_see",
1296
- "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.",
1398
+ [
1399
+ "Capture the macOS Accessibility Tree snapshot for a running application. Returns structured element list with IDs, roles, labels, and positions.",
1400
+ "",
1401
+ "WORKFLOW: Call desktop_see \u2192 find target element \u2192 use its ID in desktop_click or desktop_type.",
1402
+ "Pass the returned snapshotId to subsequent calls for 240x speed improvement (cached lookup vs. full re-scan).",
1403
+ "",
1404
+ "SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger an automatic safety stop."
1405
+ ].join("\n"),
1297
1406
  {
1298
- app: import_zod5.z.string().optional().describe("App name to target (e.g. 'Safari', 'Finder'). Omit for frontmost app.")
1407
+ app: import_zod5.z.string().optional().describe("App name to target (e.g. 'Safari', 'Notes', 'Google Chrome'). Omit for the frontmost app.")
1299
1408
  },
1300
1409
  async ({ app }) => {
1301
1410
  checkBlacklist(app);
@@ -1320,12 +1429,19 @@ var DesktopTools = class {
1320
1429
  );
1321
1430
  server.tool(
1322
1431
  "desktop_click",
1323
- "Click a UI element by label, accessibility ID, or coordinates",
1432
+ [
1433
+ "Click a macOS UI element by its accessibility label, ID, or x,y coordinates.",
1434
+ "",
1435
+ "The 'on' parameter accepts: element label text (e.g. 'Save'), accessibility ID from desktop_see, or coordinates as 'x,y' string.",
1436
+ "For faster interaction, pass the snapshotId from a recent desktop_see call.",
1437
+ "",
1438
+ "SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger automatic safety stop."
1439
+ ].join("\n"),
1324
1440
  {
1325
- on: import_zod5.z.string().describe("Element label, ID, or 'x,y' coordinates to click"),
1326
- app: import_zod5.z.string().optional().describe("App name to target"),
1327
- snapshot: import_zod5.z.string().optional().describe("snapshotId from desktop_see for cached interaction (faster)"),
1328
- doubleClick: import_zod5.z.boolean().optional().default(false).describe("Double-click")
1441
+ on: import_zod5.z.string().describe("Element label, accessibility ID, or 'x,y' coordinates to click"),
1442
+ app: import_zod5.z.string().optional().describe("App name to target (e.g. 'Safari')"),
1443
+ snapshot: import_zod5.z.string().optional().describe("snapshotId from desktop_see for cached interaction (240x faster)"),
1444
+ doubleClick: import_zod5.z.boolean().optional().default(false).describe("Double-click instead of single click")
1329
1445
  },
1330
1446
  async ({ on, app, snapshot, doubleClick }) => {
1331
1447
  checkBlacklist(app);
@@ -1341,10 +1457,14 @@ var DesktopTools = class {
1341
1457
  );
1342
1458
  server.tool(
1343
1459
  "desktop_type",
1344
- "Type text into the currently focused element",
1460
+ [
1461
+ "Type text into the currently focused UI element on macOS. The text is sent as keyboard input character-by-character.",
1462
+ "",
1463
+ "SAFETY: Terminal, iTerm, and Finder are blocked. Use desktop_see first to verify the correct element is focused."
1464
+ ].join("\n"),
1345
1465
  {
1346
- text: import_zod5.z.string().describe("Text to type"),
1347
- app: import_zod5.z.string().optional().describe("App name to target first")
1466
+ text: import_zod5.z.string().describe("Text to type into the focused element"),
1467
+ app: import_zod5.z.string().optional().describe("App name to focus before typing")
1348
1468
  },
1349
1469
  async ({ text, app }) => {
1350
1470
  checkBlacklist(app);
@@ -1358,9 +1478,15 @@ var DesktopTools = class {
1358
1478
  );
1359
1479
  server.tool(
1360
1480
  "desktop_hotkey",
1361
- "Press keyboard shortcut (e.g. 'cmd,c' for copy, 'cmd,shift,t' for new tab)",
1481
+ [
1482
+ "Press a keyboard shortcut on macOS. Keys are comma-separated.",
1483
+ "",
1484
+ "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).",
1485
+ "",
1486
+ "SAFETY: Terminal, iTerm, and Finder are blocked."
1487
+ ].join("\n"),
1362
1488
  {
1363
- keys: import_zod5.z.string().describe("Comma-separated key combination (e.g. 'cmd,c', 'cmd,shift,t', 'escape')"),
1489
+ keys: import_zod5.z.string().describe("Comma-separated key combination (e.g. 'cmd,c', 'cmd,shift,t', 'escape', 'cmd,option,i')"),
1364
1490
  app: import_zod5.z.string().optional().describe("App name to target")
1365
1491
  },
1366
1492
  async ({ keys, app }) => {
@@ -1375,11 +1501,11 @@ var DesktopTools = class {
1375
1501
  );
1376
1502
  server.tool(
1377
1503
  "desktop_scroll",
1378
- "Scroll in an app or specific element",
1504
+ "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.",
1379
1505
  {
1380
1506
  direction: import_zod5.z.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
1381
- ticks: import_zod5.z.number().optional().default(3).describe("Number of scroll ticks"),
1382
- on: import_zod5.z.string().optional().describe("Element label or ID to scroll within"),
1507
+ ticks: import_zod5.z.number().optional().default(3).describe("Number of scroll ticks (default: 3). Higher = more scrolling."),
1508
+ on: import_zod5.z.string().optional().describe("Element label or ID to scroll within (from desktop_see). Omit to scroll the active area."),
1383
1509
  app: import_zod5.z.string().optional().describe("App name to target")
1384
1510
  },
1385
1511
  async ({ direction, ticks, on, app }) => {
@@ -1395,7 +1521,7 @@ var DesktopTools = class {
1395
1521
  );
1396
1522
  server.tool(
1397
1523
  "desktop_list_apps",
1398
- "List all running applications on macOS",
1524
+ "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.).",
1399
1525
  {},
1400
1526
  async () => {
1401
1527
  try {
@@ -1411,9 +1537,9 @@ var DesktopTools = class {
1411
1537
  );
1412
1538
  server.tool(
1413
1539
  "desktop_list_windows",
1414
- "List all open windows on macOS",
1540
+ "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.",
1415
1541
  {
1416
- app: import_zod5.z.string().optional().describe("Filter by app name (omit to query frontmost app)")
1542
+ app: import_zod5.z.string().optional().describe("Filter by app name. Omit to query the frontmost app.")
1417
1543
  },
1418
1544
  async ({ app }) => {
1419
1545
  checkBlacklist(app);
@@ -1439,10 +1565,15 @@ var DesktopTools = class {
1439
1565
  );
1440
1566
  server.tool(
1441
1567
  "desktop_screenshot",
1442
- "Take macOS screen screenshot using Peekaboo (Retina support, better quality than screen_capture)",
1568
+ [
1569
+ "Take a high-quality macOS screenshot using Peekaboo (Retina display support). Returns base64 image data.",
1570
+ "",
1571
+ "MODES: 'screen' captures the full display, 'window' captures a specific app window.",
1572
+ "Prefer desktop_see (Accessibility Tree) for understanding UI structure \u2014 use screenshot only when visual appearance matters (layouts, images, colors)."
1573
+ ].join("\n"),
1443
1574
  {
1444
- app: import_zod5.z.string().optional().describe("Capture specific app window"),
1445
- mode: import_zod5.z.enum(["screen", "window"]).optional().default("screen").describe("Capture mode")
1575
+ app: import_zod5.z.string().optional().describe("Capture a specific app's window (by name)"),
1576
+ mode: import_zod5.z.enum(["screen", "window"]).optional().default("screen").describe("'screen': full display capture, 'window': specific app window only")
1446
1577
  },
1447
1578
  async ({ app, mode }) => {
1448
1579
  checkBlacklist(app);
@@ -1469,10 +1600,15 @@ var DesktopTools = class {
1469
1600
  );
1470
1601
  server.tool(
1471
1602
  "desktop_menu",
1472
- "Click menu bar item (e.g. 'File > New Tab')",
1603
+ [
1604
+ "Click a menu bar item in a macOS application. Navigate nested menus by adding path segments.",
1605
+ "",
1606
+ "Examples: ['File', 'New Tab'], ['Edit', 'Find', 'Find...'], ['View', 'Enter Full Screen'].",
1607
+ "The target app must be running and accessible."
1608
+ ].join("\n"),
1473
1609
  {
1474
- path: import_zod5.z.array(import_zod5.z.string()).describe("Menu path as array (e.g. ['File', 'New Tab'])"),
1475
- app: import_zod5.z.string().optional().describe("App name to target")
1610
+ path: import_zod5.z.array(import_zod5.z.string()).describe("Menu path as array (e.g. ['File', 'Save'], ['Edit', 'Find', 'Find...'])"),
1611
+ app: import_zod5.z.string().optional().describe("App name to target. Omit for the frontmost app.")
1476
1612
  },
1477
1613
  async ({ path: path2, app }) => {
1478
1614
  checkBlacklist(app);
@@ -1736,16 +1872,58 @@ async function handleMCPRequest(id, payload) {
1736
1872
  if (!res.ok) {
1737
1873
  throw new Error(`MCP request failed: ${res.status} ${res.statusText}`);
1738
1874
  }
1875
+ if (res.status === 202) {
1876
+ return null;
1877
+ }
1878
+ const contentType = res.headers.get("content-type") ?? "";
1879
+ if (contentType.includes("application/json")) {
1880
+ return res.json();
1881
+ }
1739
1882
  const text = await res.text();
1740
1883
  const lines = text.split("\n");
1884
+ let currentEventType = null;
1885
+ const collectedResults = [];
1886
+ let lastError = null;
1741
1887
  for (const line of lines) {
1742
- if (line.startsWith("data: ")) {
1888
+ if (line.startsWith("event: ")) {
1889
+ currentEventType = line.slice(7).trim();
1890
+ } else if (line.startsWith("data: ")) {
1891
+ const rawData = line.slice(6).trim();
1892
+ if (rawData === "") {
1893
+ currentEventType = null;
1894
+ continue;
1895
+ }
1743
1896
  try {
1744
- return JSON.parse(line.slice(6));
1897
+ const parsed = JSON.parse(rawData);
1898
+ if (currentEventType === "error") {
1899
+ lastError = parsed;
1900
+ } else if (currentEventType === "message" || currentEventType === null) {
1901
+ collectedResults.push(parsed);
1902
+ }
1745
1903
  } catch {
1746
1904
  }
1905
+ currentEventType = null;
1906
+ } else if (line === "") {
1907
+ currentEventType = null;
1747
1908
  }
1748
1909
  }
1910
+ if (collectedResults.length === 0 && lastError !== null) {
1911
+ throw new Error(
1912
+ `MCP error event: ${JSON.stringify(lastError)}`
1913
+ );
1914
+ }
1915
+ if (collectedResults.length > 1 && payload !== null && typeof payload === "object" && "id" in payload) {
1916
+ const requestId = payload.id;
1917
+ const matched = collectedResults.find(
1918
+ (r) => r !== null && typeof r === "object" && "id" in r && r.id === requestId
1919
+ );
1920
+ if (matched !== void 0) {
1921
+ return matched;
1922
+ }
1923
+ }
1924
+ if (collectedResults.length > 0) {
1925
+ return collectedResults[collectedResults.length - 1];
1926
+ }
1749
1927
  return null;
1750
1928
  }
1751
1929
  // Annotate the CommonJS export names for ESM import in node: