junis 0.3.4 → 0.3.6
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 +395 -330
- package/dist/server/mcp.js +182 -220
- package/dist/server/stdio.js +150 -183
- package/package.json +3 -2
package/dist/server/mcp.js
CHANGED
|
@@ -1,52 +1,15 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
1
|
// src/server/mcp.ts
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
handleMCPRequest: () => handleMCPRequest,
|
|
35
|
-
startMCPServer: () => startMCPServer,
|
|
36
|
-
toolPermissions: () => toolPermissions
|
|
37
|
-
});
|
|
38
|
-
module.exports = __toCommonJS(mcp_exports);
|
|
39
|
-
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
40
|
-
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
41
|
-
var import_http = require("http");
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { createServer } from "http";
|
|
42
5
|
|
|
43
6
|
// src/tools/filesystem.ts
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
7
|
+
import { exec, execFile } from "child_process";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
import fs from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { glob } from "glob";
|
|
12
|
+
import { z } from "zod";
|
|
50
13
|
|
|
51
14
|
// src/server/permissions.ts
|
|
52
15
|
var toolPermissions = {
|
|
@@ -84,9 +47,9 @@ var toolPermissions = {
|
|
|
84
47
|
cron_delete: "confirm",
|
|
85
48
|
edit_block: "confirm",
|
|
86
49
|
kill_process: "confirm",
|
|
87
|
-
// 시스템 변경 —
|
|
88
|
-
execute_command: "
|
|
89
|
-
write_file: "
|
|
50
|
+
// 시스템 변경 — 기본 차단 (PDF 7.3절)
|
|
51
|
+
execute_command: "deny",
|
|
52
|
+
write_file: "deny"
|
|
90
53
|
};
|
|
91
54
|
function checkPermission(toolName) {
|
|
92
55
|
const level = toolPermissions[toolName];
|
|
@@ -98,8 +61,8 @@ function checkPermission(toolName) {
|
|
|
98
61
|
}
|
|
99
62
|
|
|
100
63
|
// src/tools/filesystem.ts
|
|
101
|
-
var execAsync =
|
|
102
|
-
var execFileAsync =
|
|
64
|
+
var execAsync = promisify(exec);
|
|
65
|
+
var execFileAsync = promisify(execFile);
|
|
103
66
|
var FilesystemTools = class {
|
|
104
67
|
register(server) {
|
|
105
68
|
server.tool(
|
|
@@ -121,14 +84,14 @@ var FilesystemTools = class {
|
|
|
121
84
|
"- Avoid piping untrusted input into shells. Use absolute paths when possible. Quote paths containing spaces."
|
|
122
85
|
].join("\n"),
|
|
123
86
|
{
|
|
124
|
-
command:
|
|
125
|
-
timeout_ms:
|
|
126
|
-
background:
|
|
87
|
+
command: z.string().describe("The shell command to execute. Use absolute paths when possible. Quote paths containing spaces."),
|
|
88
|
+
timeout_ms: z.number().optional().default(3e4).describe("Maximum execution time in milliseconds (default: 30000). Increase for long-running builds or downloads."),
|
|
89
|
+
background: z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
|
|
127
90
|
},
|
|
128
91
|
async ({ command, timeout_ms, background }) => {
|
|
129
92
|
checkPermission("execute_command");
|
|
130
93
|
if (background) {
|
|
131
|
-
|
|
94
|
+
exec(command);
|
|
132
95
|
return { content: [{ type: "text", text: "Background execution started" }] };
|
|
133
96
|
}
|
|
134
97
|
try {
|
|
@@ -162,12 +125,12 @@ ${error.stderr ?? ""}`
|
|
|
162
125
|
"For searching within files, prefer search_code instead. For listing directory contents, use list_directory."
|
|
163
126
|
].join("\n"),
|
|
164
127
|
{
|
|
165
|
-
path:
|
|
166
|
-
encoding:
|
|
128
|
+
path: z.string().describe("Absolute or relative file path to read"),
|
|
129
|
+
encoding: z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("'utf-8' for text files (default), 'base64' for binary files (images, PDFs, archives)")
|
|
167
130
|
},
|
|
168
131
|
async ({ path: filePath, encoding }) => {
|
|
169
132
|
try {
|
|
170
|
-
const content = await
|
|
133
|
+
const content = await fs.readFile(filePath, encoding);
|
|
171
134
|
return { content: [{ type: "text", text: content }] };
|
|
172
135
|
} catch (err) {
|
|
173
136
|
const e = err;
|
|
@@ -187,13 +150,13 @@ ${error.stderr ?? ""}`
|
|
|
187
150
|
"Prefer edit_block over write_file for existing files \u2014 it's safer and preserves unmodified content."
|
|
188
151
|
].join("\n"),
|
|
189
152
|
{
|
|
190
|
-
path:
|
|
191
|
-
content:
|
|
153
|
+
path: z.string().describe("File path to create or overwrite. Parent directories are auto-created."),
|
|
154
|
+
content: z.string().describe("Complete file content. This replaces the entire file.")
|
|
192
155
|
},
|
|
193
156
|
async ({ path: filePath, content }) => {
|
|
194
157
|
checkPermission("write_file");
|
|
195
|
-
await
|
|
196
|
-
await
|
|
158
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
159
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
197
160
|
return { content: [{ type: "text", text: "File saved" }] };
|
|
198
161
|
}
|
|
199
162
|
);
|
|
@@ -204,11 +167,11 @@ ${error.stderr ?? ""}`
|
|
|
204
167
|
"Use this to explore project structure before reading or modifying files."
|
|
205
168
|
].join("\n"),
|
|
206
169
|
{
|
|
207
|
-
path:
|
|
170
|
+
path: z.string().describe("Directory path to list")
|
|
208
171
|
},
|
|
209
172
|
async ({ path: dirPath }) => {
|
|
210
173
|
try {
|
|
211
|
-
const entries = await
|
|
174
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
212
175
|
const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
|
|
213
176
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
214
177
|
} catch (err) {
|
|
@@ -229,9 +192,9 @@ ${error.stderr ?? ""}`
|
|
|
229
192
|
"Returns matching lines with file paths and line numbers for precise navigation."
|
|
230
193
|
].join("\n"),
|
|
231
194
|
{
|
|
232
|
-
pattern:
|
|
233
|
-
directory:
|
|
234
|
-
file_pattern:
|
|
195
|
+
pattern: z.string().describe("Search pattern with full regex support (e.g. 'function\\s+\\w+', 'import.*from', 'TODO')"),
|
|
196
|
+
directory: z.string().optional().default(".").describe("Root directory to search from (default: current working directory)"),
|
|
197
|
+
file_pattern: z.string().optional().default("**/*").describe("Glob pattern to filter files (e.g. '**/*.ts', '*.py', 'src/**/*.js')")
|
|
235
198
|
},
|
|
236
199
|
async ({ pattern, directory, file_pattern }) => {
|
|
237
200
|
try {
|
|
@@ -242,13 +205,13 @@ ${error.stderr ?? ""}`
|
|
|
242
205
|
);
|
|
243
206
|
return { content: [{ type: "text", text: stdout || "No results" }] };
|
|
244
207
|
} catch {
|
|
245
|
-
const safeDirectory =
|
|
246
|
-
const files = await
|
|
208
|
+
const safeDirectory = path.resolve(directory);
|
|
209
|
+
const files = await glob(file_pattern, { cwd: safeDirectory });
|
|
247
210
|
const results = [];
|
|
248
211
|
for (const file of files.slice(0, 100)) {
|
|
249
212
|
try {
|
|
250
|
-
const content = await
|
|
251
|
-
|
|
213
|
+
const content = await fs.readFile(
|
|
214
|
+
path.join(safeDirectory, file),
|
|
252
215
|
"utf-8"
|
|
253
216
|
);
|
|
254
217
|
const lines = content.split("\n");
|
|
@@ -285,8 +248,8 @@ ${error.stderr ?? ""}`
|
|
|
285
248
|
"SAFETY: Only kill processes the user explicitly identifies. Never kill system-critical processes (init, systemd, loginwindow, WindowServer) without explicit instruction."
|
|
286
249
|
].join("\n"),
|
|
287
250
|
{
|
|
288
|
-
pid:
|
|
289
|
-
signal:
|
|
251
|
+
pid: z.number().describe("PID of the process to terminate (use list_processes to find PIDs)"),
|
|
252
|
+
signal: z.enum(["SIGTERM", "SIGKILL"]).optional().default("SIGTERM").describe("SIGTERM (default): graceful shutdown with 3s auto-SIGKILL fallback. SIGKILL: immediate force kill.")
|
|
290
253
|
},
|
|
291
254
|
async ({ pid, signal }) => {
|
|
292
255
|
const isWindows = process.platform === "win32";
|
|
@@ -342,13 +305,13 @@ ${error.stderr ?? ""}`
|
|
|
342
305
|
"Prefer this over write_file for modifying existing files \u2014 it only changes what you specify and preserves the rest."
|
|
343
306
|
].join("\n"),
|
|
344
307
|
{
|
|
345
|
-
path:
|
|
346
|
-
old_string:
|
|
347
|
-
new_string:
|
|
348
|
-
replace_all:
|
|
308
|
+
path: z.string().describe("Path to the file to edit. The file must already exist."),
|
|
309
|
+
old_string: z.string().describe("The exact text to find and replace. Must match character-for-character including whitespace and newlines. Include enough context for uniqueness."),
|
|
310
|
+
new_string: z.string().describe("The replacement text. Use empty string to delete the matched text."),
|
|
311
|
+
replace_all: z.boolean().optional().default(false).describe("If true, replace ALL matches. If false (default), require exactly one match (errors on ambiguous multiple matches).")
|
|
349
312
|
},
|
|
350
313
|
async ({ path: filePath, old_string, new_string, replace_all }) => {
|
|
351
|
-
const content = await
|
|
314
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
352
315
|
if (!content.includes(old_string)) {
|
|
353
316
|
throw new Error(`old_string not found in file: ${filePath}`);
|
|
354
317
|
}
|
|
@@ -372,7 +335,7 @@ ${error.stderr ?? ""}`
|
|
|
372
335
|
result = content.replace(old_string, new_string);
|
|
373
336
|
replaced = 1;
|
|
374
337
|
}
|
|
375
|
-
await
|
|
338
|
+
await fs.writeFile(filePath, result, "utf-8");
|
|
376
339
|
return {
|
|
377
340
|
content: [{ type: "text", text: `Replaced (${replaced} occurrence(s) changed)` }]
|
|
378
341
|
};
|
|
@@ -387,9 +350,9 @@ ${error.stderr ?? ""}`
|
|
|
387
350
|
"Duplicate commands are automatically detected and rejected. Use cron_list to see existing jobs."
|
|
388
351
|
].join("\n"),
|
|
389
352
|
{
|
|
390
|
-
schedule:
|
|
391
|
-
command:
|
|
392
|
-
label:
|
|
353
|
+
schedule: z.string().describe("Cron schedule expression (5 fields: minute hour day month weekday). Examples: '*/5 * * * *' (every 5 min), '0 9 * * 1-5' (weekdays 9am)"),
|
|
354
|
+
command: z.string().describe("Shell command to execute on schedule"),
|
|
355
|
+
label: z.string().optional().describe("Human-readable label for identification (e.g. 'daily-backup', 'log-cleanup')")
|
|
393
356
|
},
|
|
394
357
|
async ({ schedule, command, label }) => {
|
|
395
358
|
try {
|
|
@@ -411,9 +374,9 @@ ${error.stderr ?? ""}`
|
|
|
411
374
|
`;
|
|
412
375
|
const updated = existing.trimEnd() + "\n" + newEntry;
|
|
413
376
|
const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
414
|
-
await
|
|
377
|
+
await fs.writeFile(tmpFile, updated, "utf-8");
|
|
415
378
|
await execAsync(`crontab ${tmpFile}`);
|
|
416
|
-
await
|
|
379
|
+
await fs.unlink(tmpFile).catch(() => {
|
|
417
380
|
});
|
|
418
381
|
return {
|
|
419
382
|
content: [{ type: "text", text: `\u2705 Cron job created:
|
|
@@ -480,8 +443,8 @@ ${error.stderr ?? ""}`
|
|
|
480
443
|
"cron_delete",
|
|
481
444
|
"Delete a scheduled cron job by its ID (from cron_list output) or by matching command string. Associated comment labels are automatically cleaned up.",
|
|
482
445
|
{
|
|
483
|
-
id:
|
|
484
|
-
command:
|
|
446
|
+
id: z.number().optional().describe("Cron job ID from cron_list output (e.g. 1, 2, 3)"),
|
|
447
|
+
command: z.string().optional().describe("Delete all jobs matching this command string")
|
|
485
448
|
},
|
|
486
449
|
async ({ id, command }) => {
|
|
487
450
|
if (!id && !command) {
|
|
@@ -518,9 +481,9 @@ ${error.stderr ?? ""}`
|
|
|
518
481
|
}
|
|
519
482
|
const updated2 = filtered2.join("\n");
|
|
520
483
|
const tmpFile2 = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
521
|
-
await
|
|
484
|
+
await fs.writeFile(tmpFile2, updated2, "utf-8");
|
|
522
485
|
await execAsync(`crontab ${tmpFile2}`);
|
|
523
|
-
await
|
|
486
|
+
await fs.unlink(tmpFile2).catch(() => {
|
|
524
487
|
});
|
|
525
488
|
return { content: [{ type: "text", text: `\u2705 Deleted cron job matching: ${command}` }] };
|
|
526
489
|
}
|
|
@@ -545,9 +508,9 @@ ${error.stderr ?? ""}`
|
|
|
545
508
|
const filtered = lines.filter((_, i) => i < target.lineStart || i > target.lineEnd);
|
|
546
509
|
const updated = filtered.join("\n");
|
|
547
510
|
const tmpFile = `/tmp/junis_crontab_${Date.now()}.txt`;
|
|
548
|
-
await
|
|
511
|
+
await fs.writeFile(tmpFile, updated, "utf-8");
|
|
549
512
|
await execAsync(`crontab ${tmpFile}`);
|
|
550
|
-
await
|
|
513
|
+
await fs.unlink(tmpFile).catch(() => {
|
|
551
514
|
});
|
|
552
515
|
return { content: [{ type: "text", text: `\u2705 Deleted cron job #${id}` }] };
|
|
553
516
|
} catch (err) {
|
|
@@ -562,9 +525,9 @@ ${error.stderr ?? ""}`
|
|
|
562
525
|
};
|
|
563
526
|
|
|
564
527
|
// src/tools/browser.ts
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
528
|
+
import { BrowserClaw } from "browserclaw";
|
|
529
|
+
import fs2 from "fs/promises";
|
|
530
|
+
import { z as z2 } from "zod";
|
|
568
531
|
var BrowserTools = class {
|
|
569
532
|
browser = null;
|
|
570
533
|
page = null;
|
|
@@ -605,11 +568,11 @@ var BrowserTools = class {
|
|
|
605
568
|
"Always call browser_stop when done to release system resources."
|
|
606
569
|
].join("\n"),
|
|
607
570
|
{
|
|
608
|
-
mode:
|
|
609
|
-
headless:
|
|
610
|
-
cdpUrl:
|
|
611
|
-
profile:
|
|
612
|
-
allowInternal:
|
|
571
|
+
mode: z2.enum(["managed", "remote-cdp"]).optional().default("managed").describe("'managed' = launch new browser, 'remote-cdp' = connect to existing Chrome via CDP"),
|
|
572
|
+
headless: z2.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
|
|
573
|
+
cdpUrl: z2.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
|
|
574
|
+
profile: z2.string().optional().describe("Browser profile name for persistent sessions \u2014 preserves cookies, logins, and history across restarts (managed mode only)"),
|
|
575
|
+
allowInternal: z2.boolean().optional().default(false).describe("Allow navigation to localhost and internal network URLs")
|
|
613
576
|
},
|
|
614
577
|
({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
|
|
615
578
|
if (this.browser) {
|
|
@@ -617,9 +580,9 @@ var BrowserTools = class {
|
|
|
617
580
|
}
|
|
618
581
|
if (mode === "remote-cdp") {
|
|
619
582
|
if (!cdpUrl) throw new Error("cdpUrl is required for remote-cdp mode");
|
|
620
|
-
this.browser = await
|
|
583
|
+
this.browser = await BrowserClaw.connect(cdpUrl, { allowInternal });
|
|
621
584
|
} else {
|
|
622
|
-
this.browser = await
|
|
585
|
+
this.browser = await BrowserClaw.launch({
|
|
623
586
|
headless,
|
|
624
587
|
profileName: profile,
|
|
625
588
|
allowInternal
|
|
@@ -641,7 +604,7 @@ var BrowserTools = class {
|
|
|
641
604
|
"browser_navigate",
|
|
642
605
|
"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.",
|
|
643
606
|
{
|
|
644
|
-
url:
|
|
607
|
+
url: z2.string().describe("Full URL to navigate to (include https://)")
|
|
645
608
|
},
|
|
646
609
|
({ url }) => this.withLock(async () => {
|
|
647
610
|
if (!this.browser) throw new Error("Browser not started. Call browser_start first.");
|
|
@@ -665,8 +628,8 @@ var BrowserTools = class {
|
|
|
665
628
|
"Prefer this over browser_screenshot for understanding page structure \u2014 it's faster, structured, and machine-readable."
|
|
666
629
|
].join("\n"),
|
|
667
630
|
{
|
|
668
|
-
interactive:
|
|
669
|
-
compact:
|
|
631
|
+
interactive: z2.boolean().optional().default(true).describe("true (default): only show clickable/typeable elements. false: show all elements including static text."),
|
|
632
|
+
compact: z2.boolean().optional().default(true).describe("true (default): hide empty containers for cleaner output")
|
|
670
633
|
},
|
|
671
634
|
({ interactive, compact }) => this.withLock(async () => {
|
|
672
635
|
const result = await requirePage().snapshot({ interactive, compact });
|
|
@@ -688,9 +651,9 @@ ${refList}`
|
|
|
688
651
|
"browser_click",
|
|
689
652
|
"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.",
|
|
690
653
|
{
|
|
691
|
-
ref:
|
|
692
|
-
doubleClick:
|
|
693
|
-
button:
|
|
654
|
+
ref: z2.string().describe("Element ref from browser_snapshot (e.g. 'e1', 'e15'). Call browser_snapshot first to get current refs."),
|
|
655
|
+
doubleClick: z2.boolean().optional().default(false).describe("Double-click instead of single click"),
|
|
656
|
+
button: z2.enum(["left", "right", "middle"]).optional().default("left").describe("Mouse button to use")
|
|
694
657
|
},
|
|
695
658
|
({ ref, doubleClick, button }) => this.withLock(async () => {
|
|
696
659
|
await requirePage().click(ref, { doubleClick, button });
|
|
@@ -701,10 +664,10 @@ ${refList}`
|
|
|
701
664
|
"browser_type",
|
|
702
665
|
"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.",
|
|
703
666
|
{
|
|
704
|
-
ref:
|
|
705
|
-
text:
|
|
706
|
-
submit:
|
|
707
|
-
slowly:
|
|
667
|
+
ref: z2.string().describe("Element ref from browser_snapshot (e.g. 'e3')"),
|
|
668
|
+
text: z2.string().describe("Text to type into the element"),
|
|
669
|
+
submit: z2.boolean().optional().default(false).describe("Press Enter after typing (useful for search boxes and forms)"),
|
|
670
|
+
slowly: z2.boolean().optional().default(false).describe("Type slowly (75ms per char) for sites that process each keystroke")
|
|
708
671
|
},
|
|
709
672
|
({ ref, text, submit, slowly }) => this.withLock(async () => {
|
|
710
673
|
await requirePage().type(ref, text, { submit, slowly });
|
|
@@ -715,10 +678,10 @@ ${refList}`
|
|
|
715
678
|
"browser_fill",
|
|
716
679
|
"Fill multiple form fields at once \u2014 more efficient than calling browser_type repeatedly. Each field needs a ref from browser_snapshot.",
|
|
717
680
|
{
|
|
718
|
-
fields:
|
|
719
|
-
ref:
|
|
720
|
-
type:
|
|
721
|
-
value:
|
|
681
|
+
fields: z2.array(z2.object({
|
|
682
|
+
ref: z2.string(),
|
|
683
|
+
type: z2.enum(["text", "checkbox", "radio"]),
|
|
684
|
+
value: z2.union([z2.string(), z2.boolean()])
|
|
722
685
|
})).describe("Array of {ref, type, value}. type='text': value is string. type='checkbox'/'radio': value is boolean.")
|
|
723
686
|
},
|
|
724
687
|
({ fields }) => this.withLock(async () => {
|
|
@@ -730,8 +693,8 @@ ${refList}`
|
|
|
730
693
|
"browser_select",
|
|
731
694
|
"Select one or more options from a dropdown/select element. Values should match the option value attributes, not display text.",
|
|
732
695
|
{
|
|
733
|
-
ref:
|
|
734
|
-
values:
|
|
696
|
+
ref: z2.string().describe("Ref of the <select> element from browser_snapshot"),
|
|
697
|
+
values: z2.array(z2.string()).describe("Option value(s) to select")
|
|
735
698
|
},
|
|
736
699
|
({ ref, values }) => this.withLock(async () => {
|
|
737
700
|
await requirePage().select(ref, ...values);
|
|
@@ -742,7 +705,7 @@ ${refList}`
|
|
|
742
705
|
"browser_press",
|
|
743
706
|
"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.",
|
|
744
707
|
{
|
|
745
|
-
key:
|
|
708
|
+
key: z2.string().describe("Key or combination: 'Enter', 'Escape', 'Tab', 'Control+a', 'Meta+c', 'ArrowDown', 'Backspace'")
|
|
746
709
|
},
|
|
747
710
|
({ key }) => this.withLock(async () => {
|
|
748
711
|
await requirePage().press(key);
|
|
@@ -753,7 +716,7 @@ ${refList}`
|
|
|
753
716
|
"browser_hover",
|
|
754
717
|
"Move the mouse cursor over an element by ref. Use to trigger hover menus, tooltips, or dropdown previews before clicking.",
|
|
755
718
|
{
|
|
756
|
-
ref:
|
|
719
|
+
ref: z2.string().describe("Element ref from browser_snapshot")
|
|
757
720
|
},
|
|
758
721
|
({ ref }) => this.withLock(async () => {
|
|
759
722
|
await requirePage().hover(ref);
|
|
@@ -764,8 +727,8 @@ ${refList}`
|
|
|
764
727
|
"browser_drag",
|
|
765
728
|
"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.",
|
|
766
729
|
{
|
|
767
|
-
startRef:
|
|
768
|
-
endRef:
|
|
730
|
+
startRef: z2.string().describe("Source element ref to drag from"),
|
|
731
|
+
endRef: z2.string().describe("Target element ref to drag to")
|
|
769
732
|
},
|
|
770
733
|
({ startRef, endRef }) => this.withLock(async () => {
|
|
771
734
|
await requirePage().drag(startRef, endRef);
|
|
@@ -776,8 +739,8 @@ ${refList}`
|
|
|
776
739
|
"browser_upload",
|
|
777
740
|
"Upload local files to a file input element (<input type='file'>). The ref must point to a file input from browser_snapshot.",
|
|
778
741
|
{
|
|
779
|
-
ref:
|
|
780
|
-
paths:
|
|
742
|
+
ref: z2.string().describe("Ref of the file input element from browser_snapshot"),
|
|
743
|
+
paths: z2.array(z2.string()).describe("Absolute file path(s) on the local device to upload")
|
|
781
744
|
},
|
|
782
745
|
({ ref, paths }) => this.withLock(async () => {
|
|
783
746
|
await requirePage().uploadFile(ref, paths);
|
|
@@ -793,14 +756,14 @@ ${refList}`
|
|
|
793
756
|
"Use browser_screenshot only when visual layout matters (charts, images, styling, visual verification)."
|
|
794
757
|
].join("\n"),
|
|
795
758
|
{
|
|
796
|
-
path:
|
|
797
|
-
fullPage:
|
|
798
|
-
ref:
|
|
759
|
+
path: z2.string().optional().describe("Save path for the screenshot. If omitted, returns base64 image data directly."),
|
|
760
|
+
fullPage: z2.boolean().optional().default(false).describe("Capture the full scrollable page, not just the visible viewport"),
|
|
761
|
+
ref: z2.string().optional().describe("Capture only a specific element by its ref from browser_snapshot")
|
|
799
762
|
},
|
|
800
763
|
({ path: path2, fullPage, ref }) => this.withLock(async () => {
|
|
801
764
|
const buffer = await requirePage().screenshot({ fullPage, ref });
|
|
802
765
|
if (path2) {
|
|
803
|
-
await
|
|
766
|
+
await fs2.writeFile(path2, buffer);
|
|
804
767
|
return { content: [{ type: "text", text: `Screenshot saved: ${path2}` }] };
|
|
805
768
|
}
|
|
806
769
|
return {
|
|
@@ -816,11 +779,11 @@ ${refList}`
|
|
|
816
779
|
"browser_pdf",
|
|
817
780
|
"Save the current page as a PDF file. Renders the full page including below-the-fold content. Useful for archiving, sharing, or offline reading.",
|
|
818
781
|
{
|
|
819
|
-
path:
|
|
782
|
+
path: z2.string().describe("Output file path (.pdf)")
|
|
820
783
|
},
|
|
821
784
|
({ path: path2 }) => this.withLock(async () => {
|
|
822
785
|
const buffer = await requirePage().pdf();
|
|
823
|
-
await
|
|
786
|
+
await fs2.writeFile(path2, buffer);
|
|
824
787
|
return { content: [{ type: "text", text: `PDF saved: ${path2}` }] };
|
|
825
788
|
})
|
|
826
789
|
);
|
|
@@ -833,7 +796,7 @@ ${refList}`
|
|
|
833
796
|
"Wrap complex logic in an IIFE: (function(){ ... })()"
|
|
834
797
|
].join("\n"),
|
|
835
798
|
{
|
|
836
|
-
code:
|
|
799
|
+
code: z2.string().describe("JavaScript code to execute in the page context. Return values are automatically serialized.")
|
|
837
800
|
},
|
|
838
801
|
({ code }) => this.withLock(async () => {
|
|
839
802
|
try {
|
|
@@ -860,11 +823,11 @@ ${refList}`
|
|
|
860
823
|
"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
824
|
].join("\n"),
|
|
862
825
|
{
|
|
863
|
-
text:
|
|
864
|
-
textGone:
|
|
865
|
-
url:
|
|
866
|
-
loadState:
|
|
867
|
-
timeMs:
|
|
826
|
+
text: z2.string().optional().describe("Wait until this text appears on the page"),
|
|
827
|
+
textGone: z2.string().optional().describe("Wait until this text disappears from the page"),
|
|
828
|
+
url: z2.string().optional().describe("Wait until URL matches this glob pattern (e.g. '**/dashboard', '**/success')"),
|
|
829
|
+
loadState: z2.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait for page load state: 'load' (full), 'domcontentloaded' (DOM ready), 'networkidle' (no pending requests)"),
|
|
830
|
+
timeMs: z2.number().optional().describe("Fixed wait in milliseconds \u2014 use as last resort when other conditions don't apply")
|
|
868
831
|
},
|
|
869
832
|
({ text, textGone, url, loadState, timeMs }) => this.withLock(async () => {
|
|
870
833
|
const condition = {};
|
|
@@ -881,14 +844,14 @@ ${refList}`
|
|
|
881
844
|
"browser_cookies",
|
|
882
845
|
"Manage browser cookies: get all cookies, set a specific cookie, or clear all cookies. Useful for authentication state, session management, or testing.",
|
|
883
846
|
{
|
|
884
|
-
action:
|
|
885
|
-
cookie:
|
|
886
|
-
name:
|
|
887
|
-
value:
|
|
888
|
-
domain:
|
|
889
|
-
path:
|
|
890
|
-
httpOnly:
|
|
891
|
-
secure:
|
|
847
|
+
action: z2.enum(["get", "set", "clear"]).describe("'get': retrieve all cookies, 'set': add/update a cookie, 'clear': remove all cookies"),
|
|
848
|
+
cookie: z2.object({
|
|
849
|
+
name: z2.string(),
|
|
850
|
+
value: z2.string(),
|
|
851
|
+
domain: z2.string().optional(),
|
|
852
|
+
path: z2.string().optional(),
|
|
853
|
+
httpOnly: z2.boolean().optional(),
|
|
854
|
+
secure: z2.boolean().optional()
|
|
892
855
|
}).optional().describe("Cookie data (required for 'set' action)")
|
|
893
856
|
},
|
|
894
857
|
({ action, cookie }) => this.withLock(async () => {
|
|
@@ -910,10 +873,10 @@ ${refList}`
|
|
|
910
873
|
"browser_storage",
|
|
911
874
|
"Read, write, or clear browser localStorage/sessionStorage. Useful for managing client-side state, authentication tokens, or application preferences.",
|
|
912
875
|
{
|
|
913
|
-
action:
|
|
914
|
-
kind:
|
|
915
|
-
key:
|
|
916
|
-
value:
|
|
876
|
+
action: z2.enum(["get", "set", "clear"]).describe("'get': read value(s), 'set': write a key-value pair, 'clear': remove all entries"),
|
|
877
|
+
kind: z2.enum(["local", "session"]).optional().default("local").describe("'local' (persistent) or 'session' (cleared on tab close)"),
|
|
878
|
+
key: z2.string().optional().describe("Storage key to get or set. Omit key with 'get' to retrieve all entries."),
|
|
879
|
+
value: z2.string().optional().describe("Value to store (required for 'set' action)")
|
|
917
880
|
},
|
|
918
881
|
({ action, kind, key, value }) => this.withLock(async () => {
|
|
919
882
|
const page = requirePage();
|
|
@@ -941,16 +904,16 @@ ${refList}`
|
|
|
941
904
|
"The 'accept' and 'promptText' params are only used with action='arm'."
|
|
942
905
|
].join("\n"),
|
|
943
906
|
{
|
|
944
|
-
action:
|
|
907
|
+
action: z2.enum(["arm", "wait"]).describe(
|
|
945
908
|
"'arm' = register handler and return immediately; 'wait' = await the previously armed handler"
|
|
946
909
|
),
|
|
947
|
-
accept:
|
|
910
|
+
accept: z2.boolean().optional().default(true).describe(
|
|
948
911
|
"Accept (true) or dismiss (false) the dialog. Only used with action='arm'."
|
|
949
912
|
),
|
|
950
|
-
promptText:
|
|
913
|
+
promptText: z2.string().optional().describe(
|
|
951
914
|
"Text to enter if the dialog is a prompt. Only used with action='arm'."
|
|
952
915
|
),
|
|
953
|
-
timeoutMs:
|
|
916
|
+
timeoutMs: z2.number().optional().describe(
|
|
954
917
|
"Timeout in ms for 'wait' action (default: 30000). Increase for slow-loading dialogs."
|
|
955
918
|
)
|
|
956
919
|
},
|
|
@@ -982,13 +945,13 @@ ${refList}`
|
|
|
982
945
|
};
|
|
983
946
|
|
|
984
947
|
// src/tools/notebook.ts
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
var execAsync2 = (
|
|
948
|
+
import { z as z3 } from "zod";
|
|
949
|
+
import fs3 from "fs/promises";
|
|
950
|
+
import { exec as exec2 } from "child_process";
|
|
951
|
+
import { promisify as promisify2 } from "util";
|
|
952
|
+
var execAsync2 = promisify2(exec2);
|
|
990
953
|
async function readNotebook(filePath) {
|
|
991
|
-
const raw = await
|
|
954
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
992
955
|
try {
|
|
993
956
|
return JSON.parse(raw);
|
|
994
957
|
} catch {
|
|
@@ -996,14 +959,14 @@ async function readNotebook(filePath) {
|
|
|
996
959
|
}
|
|
997
960
|
}
|
|
998
961
|
async function writeNotebook(filePath, nb) {
|
|
999
|
-
await
|
|
962
|
+
await fs3.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
|
|
1000
963
|
}
|
|
1001
964
|
var NotebookTools = class {
|
|
1002
965
|
register(server) {
|
|
1003
966
|
server.tool(
|
|
1004
967
|
"notebook_read",
|
|
1005
968
|
"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:
|
|
969
|
+
{ path: z3.string().describe("Path to the .ipynb notebook file") },
|
|
1007
970
|
async ({ path: filePath }) => {
|
|
1008
971
|
const nb = await readNotebook(filePath);
|
|
1009
972
|
const cells = nb.cells.map((cell, i) => ({
|
|
@@ -1021,9 +984,9 @@ var NotebookTools = class {
|
|
|
1021
984
|
"notebook_edit_cell",
|
|
1022
985
|
"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.",
|
|
1023
986
|
{
|
|
1024
|
-
path:
|
|
1025
|
-
cell_index:
|
|
1026
|
-
source:
|
|
987
|
+
path: z3.string().describe("Path to the .ipynb notebook file"),
|
|
988
|
+
cell_index: z3.number().describe("Cell index to edit (0-based). Use notebook_read to find the right index."),
|
|
989
|
+
source: z3.string().describe("New source code/content for the cell (replaces entire cell content)")
|
|
1027
990
|
},
|
|
1028
991
|
async ({ path: filePath, cell_index, source }) => {
|
|
1029
992
|
const nb = await readNotebook(filePath);
|
|
@@ -1046,8 +1009,8 @@ var NotebookTools = class {
|
|
|
1046
1009
|
"If execution fails on a cell, the error is captured in the cell output and subsequent cells may not execute."
|
|
1047
1010
|
].join("\n"),
|
|
1048
1011
|
{
|
|
1049
|
-
path:
|
|
1050
|
-
timeout:
|
|
1012
|
+
path: z3.string().describe("Path to the .ipynb notebook file to execute"),
|
|
1013
|
+
timeout: z3.number().optional().default(300).describe("Maximum execution time per cell in seconds (default: 300). Increase for cells with heavy computation.")
|
|
1051
1014
|
},
|
|
1052
1015
|
async ({ path: filePath, timeout }) => {
|
|
1053
1016
|
const nbconvertArgs = `nbconvert --to notebook --execute --inplace "${filePath}" --ExecutePreprocessor.timeout=${timeout}`;
|
|
@@ -1078,10 +1041,10 @@ var NotebookTools = class {
|
|
|
1078
1041
|
"notebook_add_cell",
|
|
1079
1042
|
"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.",
|
|
1080
1043
|
{
|
|
1081
|
-
path:
|
|
1082
|
-
cell_type:
|
|
1083
|
-
source:
|
|
1084
|
-
position:
|
|
1044
|
+
path: z3.string().describe("Path to the .ipynb notebook file"),
|
|
1045
|
+
cell_type: z3.enum(["code", "markdown"]).describe("'code' for executable cells, 'markdown' for text/documentation cells"),
|
|
1046
|
+
source: z3.string().describe("Cell source content (Python code or Markdown text)"),
|
|
1047
|
+
position: z3.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.")
|
|
1085
1048
|
},
|
|
1086
1049
|
async ({ path: filePath, cell_type: cellType, source, position }) => {
|
|
1087
1050
|
const nb = await readNotebook(filePath);
|
|
@@ -1114,8 +1077,8 @@ var NotebookTools = class {
|
|
|
1114
1077
|
"notebook_delete_cell",
|
|
1115
1078
|
"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.",
|
|
1116
1079
|
{
|
|
1117
|
-
path:
|
|
1118
|
-
cell_index:
|
|
1080
|
+
path: z3.string().describe("Path to the .ipynb notebook file"),
|
|
1081
|
+
cell_index: z3.number().describe("Cell index to delete (0-based). Use notebook_read first to verify content.")
|
|
1119
1082
|
},
|
|
1120
1083
|
async ({ path: filePath, cell_index }) => {
|
|
1121
1084
|
const nb = await readNotebook(filePath);
|
|
@@ -1131,11 +1094,11 @@ var NotebookTools = class {
|
|
|
1131
1094
|
};
|
|
1132
1095
|
|
|
1133
1096
|
// src/tools/device.ts
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
var execAsync3 = (
|
|
1097
|
+
import { exec as exec3 } from "child_process";
|
|
1098
|
+
import { promisify as promisify3 } from "util";
|
|
1099
|
+
import { z as z4 } from "zod";
|
|
1100
|
+
import notifier from "node-notifier";
|
|
1101
|
+
var execAsync3 = promisify3(exec3);
|
|
1139
1102
|
var screenRecordPid = null;
|
|
1140
1103
|
function platform() {
|
|
1141
1104
|
if (process.platform === "darwin") return "mac";
|
|
@@ -1153,7 +1116,7 @@ var DeviceTools = class {
|
|
|
1153
1116
|
"Requires a connected camera with OS permissions granted. If output_path is provided, the file is also saved to disk."
|
|
1154
1117
|
].join("\n"),
|
|
1155
1118
|
{
|
|
1156
|
-
output_path:
|
|
1119
|
+
output_path: z4.string().optional().describe("File path to save the captured photo. If omitted, returns image data only (temp file auto-cleaned).")
|
|
1157
1120
|
},
|
|
1158
1121
|
async ({ output_path }) => {
|
|
1159
1122
|
const p = platform();
|
|
@@ -1191,13 +1154,13 @@ Please check if a camera is connected.` }],
|
|
|
1191
1154
|
"notification_send",
|
|
1192
1155
|
"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.",
|
|
1193
1156
|
{
|
|
1194
|
-
title:
|
|
1195
|
-
message:
|
|
1157
|
+
title: z4.string().describe("Notification title (displayed prominently)"),
|
|
1158
|
+
message: z4.string().describe("Notification body text")
|
|
1196
1159
|
},
|
|
1197
1160
|
async ({ title, message }) => {
|
|
1198
1161
|
try {
|
|
1199
1162
|
await new Promise((resolve, reject) => {
|
|
1200
|
-
|
|
1163
|
+
notifier.notify(
|
|
1201
1164
|
{ title, message },
|
|
1202
1165
|
(err) => {
|
|
1203
1166
|
if (err) reject(err);
|
|
@@ -1229,7 +1192,7 @@ Please check if a camera is connected.` }],
|
|
|
1229
1192
|
"clipboard_write",
|
|
1230
1193
|
"Write text to the system clipboard, replacing its current contents. Use to prepare content for the user to paste elsewhere.",
|
|
1231
1194
|
{
|
|
1232
|
-
text:
|
|
1195
|
+
text: z4.string().describe("Text to copy to the clipboard")
|
|
1233
1196
|
},
|
|
1234
1197
|
async ({ text }) => {
|
|
1235
1198
|
const p = platform();
|
|
@@ -1251,8 +1214,8 @@ Please check if a camera is connected.` }],
|
|
|
1251
1214
|
"Platform-specific: macOS (screencapture -v), Windows/Linux (ffmpeg)."
|
|
1252
1215
|
].join("\n"),
|
|
1253
1216
|
{
|
|
1254
|
-
action:
|
|
1255
|
-
output_path:
|
|
1217
|
+
action: z4.enum(["start", "stop"]).describe("'start': begin recording, 'stop': end recording and save the file"),
|
|
1218
|
+
output_path: z4.string().optional().describe("Output file path (used with 'start'). Default: /tmp/junis_record_<timestamp>.mp4")
|
|
1256
1219
|
},
|
|
1257
1220
|
async ({ action, output_path }) => {
|
|
1258
1221
|
const p = platform();
|
|
@@ -1317,7 +1280,7 @@ Please check if a camera is connected.` }],
|
|
|
1317
1280
|
"audio_play",
|
|
1318
1281
|
"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).",
|
|
1319
1282
|
{
|
|
1320
|
-
file_path:
|
|
1283
|
+
file_path: z4.string().describe("Absolute path to the audio file to play")
|
|
1321
1284
|
},
|
|
1322
1285
|
async ({ file_path }) => {
|
|
1323
1286
|
const p = platform();
|
|
@@ -1334,12 +1297,12 @@ Please check if a camera is connected.` }],
|
|
|
1334
1297
|
};
|
|
1335
1298
|
|
|
1336
1299
|
// src/setup/peekaboo-installer.ts
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
var execFileAsync2 = (
|
|
1300
|
+
import { execFile as execFile2 } from "child_process";
|
|
1301
|
+
import { promisify as promisify4 } from "util";
|
|
1302
|
+
import { platform as platform2 } from "os";
|
|
1303
|
+
var execFileAsync2 = promisify4(execFile2);
|
|
1341
1304
|
async function ensurePeekaboo() {
|
|
1342
|
-
if ((
|
|
1305
|
+
if (platform2() !== "darwin") return false;
|
|
1343
1306
|
try {
|
|
1344
1307
|
await execFileAsync2("which", ["peekaboo"]);
|
|
1345
1308
|
return true;
|
|
@@ -1359,9 +1322,9 @@ async function ensurePeekaboo() {
|
|
|
1359
1322
|
}
|
|
1360
1323
|
|
|
1361
1324
|
// src/tools/desktop.ts
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1325
|
+
import { execa } from "execa";
|
|
1326
|
+
import { z as z5 } from "zod";
|
|
1327
|
+
import fs4 from "fs";
|
|
1365
1328
|
var APP_BLACKLIST = /* @__PURE__ */ new Set([
|
|
1366
1329
|
"Terminal",
|
|
1367
1330
|
"iTerm2",
|
|
@@ -1374,7 +1337,7 @@ var MAX_CONSECUTIVE_FAILURES = 2;
|
|
|
1374
1337
|
async function peekaboo(args) {
|
|
1375
1338
|
consecutiveFailures = 0;
|
|
1376
1339
|
try {
|
|
1377
|
-
const { stdout } = await
|
|
1340
|
+
const { stdout } = await execa("peekaboo", [...args, "--json-output"]);
|
|
1378
1341
|
consecutiveFailures = 0;
|
|
1379
1342
|
return JSON.parse(stdout);
|
|
1380
1343
|
} catch (err) {
|
|
@@ -1404,7 +1367,7 @@ var DesktopTools = class {
|
|
|
1404
1367
|
"SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger an automatic safety stop."
|
|
1405
1368
|
].join("\n"),
|
|
1406
1369
|
{
|
|
1407
|
-
app:
|
|
1370
|
+
app: z5.string().optional().describe("App name to target (e.g. 'Safari', 'Notes', 'Google Chrome'). Omit for the frontmost app.")
|
|
1408
1371
|
},
|
|
1409
1372
|
async ({ app }) => {
|
|
1410
1373
|
checkBlacklist(app);
|
|
@@ -1438,10 +1401,10 @@ var DesktopTools = class {
|
|
|
1438
1401
|
"SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger automatic safety stop."
|
|
1439
1402
|
].join("\n"),
|
|
1440
1403
|
{
|
|
1441
|
-
on:
|
|
1442
|
-
app:
|
|
1443
|
-
snapshot:
|
|
1444
|
-
doubleClick:
|
|
1404
|
+
on: z5.string().describe("Element label, accessibility ID, or 'x,y' coordinates to click"),
|
|
1405
|
+
app: z5.string().optional().describe("App name to target (e.g. 'Safari')"),
|
|
1406
|
+
snapshot: z5.string().optional().describe("snapshotId from desktop_see for cached interaction (240x faster)"),
|
|
1407
|
+
doubleClick: z5.boolean().optional().default(false).describe("Double-click instead of single click")
|
|
1445
1408
|
},
|
|
1446
1409
|
async ({ on, app, snapshot, doubleClick }) => {
|
|
1447
1410
|
checkBlacklist(app);
|
|
@@ -1463,8 +1426,8 @@ var DesktopTools = class {
|
|
|
1463
1426
|
"SAFETY: Terminal, iTerm, and Finder are blocked. Use desktop_see first to verify the correct element is focused."
|
|
1464
1427
|
].join("\n"),
|
|
1465
1428
|
{
|
|
1466
|
-
text:
|
|
1467
|
-
app:
|
|
1429
|
+
text: z5.string().describe("Text to type into the focused element"),
|
|
1430
|
+
app: z5.string().optional().describe("App name to focus before typing")
|
|
1468
1431
|
},
|
|
1469
1432
|
async ({ text, app }) => {
|
|
1470
1433
|
checkBlacklist(app);
|
|
@@ -1486,8 +1449,8 @@ var DesktopTools = class {
|
|
|
1486
1449
|
"SAFETY: Terminal, iTerm, and Finder are blocked."
|
|
1487
1450
|
].join("\n"),
|
|
1488
1451
|
{
|
|
1489
|
-
keys:
|
|
1490
|
-
app:
|
|
1452
|
+
keys: z5.string().describe("Comma-separated key combination (e.g. 'cmd,c', 'cmd,shift,t', 'escape', 'cmd,option,i')"),
|
|
1453
|
+
app: z5.string().optional().describe("App name to target")
|
|
1491
1454
|
},
|
|
1492
1455
|
async ({ keys, app }) => {
|
|
1493
1456
|
checkBlacklist(app);
|
|
@@ -1503,10 +1466,10 @@ var DesktopTools = class {
|
|
|
1503
1466
|
"desktop_scroll",
|
|
1504
1467
|
"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.",
|
|
1505
1468
|
{
|
|
1506
|
-
direction:
|
|
1507
|
-
ticks:
|
|
1508
|
-
on:
|
|
1509
|
-
app:
|
|
1469
|
+
direction: z5.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
|
|
1470
|
+
ticks: z5.number().optional().default(3).describe("Number of scroll ticks (default: 3). Higher = more scrolling."),
|
|
1471
|
+
on: z5.string().optional().describe("Element label or ID to scroll within (from desktop_see). Omit to scroll the active area."),
|
|
1472
|
+
app: z5.string().optional().describe("App name to target")
|
|
1510
1473
|
},
|
|
1511
1474
|
async ({ direction, ticks, on, app }) => {
|
|
1512
1475
|
checkBlacklist(app);
|
|
@@ -1525,7 +1488,7 @@ var DesktopTools = class {
|
|
|
1525
1488
|
{},
|
|
1526
1489
|
async () => {
|
|
1527
1490
|
try {
|
|
1528
|
-
const { stdout } = await
|
|
1491
|
+
const { stdout } = await execa("peekaboo", ["list", "apps", "--json"]);
|
|
1529
1492
|
return {
|
|
1530
1493
|
content: [{ type: "text", text: stdout }]
|
|
1531
1494
|
};
|
|
@@ -1539,21 +1502,21 @@ var DesktopTools = class {
|
|
|
1539
1502
|
"desktop_list_windows",
|
|
1540
1503
|
"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.",
|
|
1541
1504
|
{
|
|
1542
|
-
app:
|
|
1505
|
+
app: z5.string().optional().describe("Filter by app name. Omit to query the frontmost app.")
|
|
1543
1506
|
},
|
|
1544
1507
|
async ({ app }) => {
|
|
1545
1508
|
checkBlacklist(app);
|
|
1546
1509
|
try {
|
|
1547
1510
|
let targetApp = app;
|
|
1548
1511
|
if (!targetApp) {
|
|
1549
|
-
const { stdout: stdout2 } = await
|
|
1512
|
+
const { stdout: stdout2 } = await execa("osascript", [
|
|
1550
1513
|
"-e",
|
|
1551
1514
|
'tell application "System Events" to get name of first application process whose frontmost is true'
|
|
1552
1515
|
]);
|
|
1553
1516
|
targetApp = stdout2.trim();
|
|
1554
1517
|
}
|
|
1555
1518
|
const args = ["list", "windows", "--app", targetApp, "--json"];
|
|
1556
|
-
const { stdout } = await
|
|
1519
|
+
const { stdout } = await execa("peekaboo", args);
|
|
1557
1520
|
return {
|
|
1558
1521
|
content: [{ type: "text", text: stdout }]
|
|
1559
1522
|
};
|
|
@@ -1572,8 +1535,8 @@ var DesktopTools = class {
|
|
|
1572
1535
|
"Prefer desktop_see (Accessibility Tree) for understanding UI structure \u2014 use screenshot only when visual appearance matters (layouts, images, colors)."
|
|
1573
1536
|
].join("\n"),
|
|
1574
1537
|
{
|
|
1575
|
-
app:
|
|
1576
|
-
mode:
|
|
1538
|
+
app: z5.string().optional().describe("Capture a specific app's window (by name)"),
|
|
1539
|
+
mode: z5.enum(["screen", "window"]).optional().default("screen").describe("'screen': full display capture, 'window': specific app window only")
|
|
1577
1540
|
},
|
|
1578
1541
|
async ({ app, mode }) => {
|
|
1579
1542
|
checkBlacklist(app);
|
|
@@ -1584,7 +1547,7 @@ var DesktopTools = class {
|
|
|
1584
1547
|
const files = data?.files;
|
|
1585
1548
|
const filePath = files?.[0]?.path;
|
|
1586
1549
|
if (filePath) {
|
|
1587
|
-
const imageBuffer =
|
|
1550
|
+
const imageBuffer = fs4.readFileSync(filePath);
|
|
1588
1551
|
return {
|
|
1589
1552
|
content: [{
|
|
1590
1553
|
type: "image",
|
|
@@ -1607,15 +1570,15 @@ var DesktopTools = class {
|
|
|
1607
1570
|
"The target app must be running and accessible."
|
|
1608
1571
|
].join("\n"),
|
|
1609
1572
|
{
|
|
1610
|
-
path:
|
|
1611
|
-
app:
|
|
1573
|
+
path: z5.array(z5.string()).describe("Menu path as array (e.g. ['File', 'Save'], ['Edit', 'Find', 'Find...'])"),
|
|
1574
|
+
app: z5.string().optional().describe("App name to target. Omit for the frontmost app.")
|
|
1612
1575
|
},
|
|
1613
1576
|
async ({ path: path2, app }) => {
|
|
1614
1577
|
checkBlacklist(app);
|
|
1615
1578
|
const args = ["menu", "click", "--path", path2.join(" > ")];
|
|
1616
1579
|
if (app) args.push("--app", app);
|
|
1617
1580
|
try {
|
|
1618
|
-
const { stdout } = await
|
|
1581
|
+
const { stdout } = await execa("peekaboo", args);
|
|
1619
1582
|
return {
|
|
1620
1583
|
content: [{ type: "text", text: stdout || "Menu click executed" }]
|
|
1621
1584
|
};
|
|
@@ -1633,7 +1596,7 @@ var mcpPort = 3e3;
|
|
|
1633
1596
|
var globalBrowserTools = null;
|
|
1634
1597
|
var desktopToolsEnabled = false;
|
|
1635
1598
|
function createMcpServer() {
|
|
1636
|
-
const server = new
|
|
1599
|
+
const server = new McpServer({
|
|
1637
1600
|
name: "junis",
|
|
1638
1601
|
version: "0.1.0"
|
|
1639
1602
|
});
|
|
@@ -1763,7 +1726,7 @@ async function startMCPServer(port) {
|
|
|
1763
1726
|
console.log("\u2705 Peekaboo available \u2014 desktop tools enabled");
|
|
1764
1727
|
}
|
|
1765
1728
|
let resolvedPort = port;
|
|
1766
|
-
const httpServer =
|
|
1729
|
+
const httpServer = createServer(
|
|
1767
1730
|
async (req, res) => {
|
|
1768
1731
|
try {
|
|
1769
1732
|
if (req.method === "OPTIONS") {
|
|
@@ -1777,7 +1740,7 @@ async function startMCPServer(port) {
|
|
|
1777
1740
|
if (url === "/mcp") {
|
|
1778
1741
|
if (req.method === "POST") {
|
|
1779
1742
|
const mcpServer = createMcpServer();
|
|
1780
|
-
const transport = new
|
|
1743
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1781
1744
|
sessionIdGenerator: void 0
|
|
1782
1745
|
// stateless
|
|
1783
1746
|
});
|
|
@@ -1926,10 +1889,9 @@ async function handleMCPRequest(id, payload) {
|
|
|
1926
1889
|
}
|
|
1927
1890
|
return null;
|
|
1928
1891
|
}
|
|
1929
|
-
|
|
1930
|
-
0 && (module.exports = {
|
|
1892
|
+
export {
|
|
1931
1893
|
checkPermission,
|
|
1932
1894
|
handleMCPRequest,
|
|
1933
1895
|
startMCPServer,
|
|
1934
1896
|
toolPermissions
|
|
1935
|
-
}
|
|
1897
|
+
};
|