junis 0.3.8 → 0.3.10
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 +159 -18
- package/dist/server/mcp.js +35 -17
- package/dist/server/stdio.js +16 -15
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -165,6 +165,40 @@ function sleep(ms) {
|
|
|
165
165
|
|
|
166
166
|
// src/relay/client.ts
|
|
167
167
|
import WebSocket from "ws";
|
|
168
|
+
|
|
169
|
+
// src/relay/upload.ts
|
|
170
|
+
var LARGE_FILE_THRESHOLD = 5 * 1024 * 1024;
|
|
171
|
+
async function uploadLargeFile(relay, base64Data, filename, contentType) {
|
|
172
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
173
|
+
const { put_url, access_url } = await relay.requestUploadUrl(
|
|
174
|
+
filename,
|
|
175
|
+
contentType,
|
|
176
|
+
buffer.length
|
|
177
|
+
);
|
|
178
|
+
const res = await fetch(put_url, {
|
|
179
|
+
method: "PUT",
|
|
180
|
+
headers: { "Content-Type": contentType },
|
|
181
|
+
body: buffer
|
|
182
|
+
});
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
throw new Error(`Upload failed: ${res.status} ${res.statusText}`);
|
|
185
|
+
}
|
|
186
|
+
return access_url;
|
|
187
|
+
}
|
|
188
|
+
function isLargeBase64(base64) {
|
|
189
|
+
return base64.length * 0.75 > LARGE_FILE_THRESHOLD;
|
|
190
|
+
}
|
|
191
|
+
function detectContentType(base64) {
|
|
192
|
+
const header = base64.slice(0, 16);
|
|
193
|
+
if (header.startsWith("/9j/")) return "image/jpeg";
|
|
194
|
+
if (header.startsWith("iVBOR")) return "image/png";
|
|
195
|
+
if (header.startsWith("R0lGO")) return "image/gif";
|
|
196
|
+
if (header.startsWith("UklGR")) return "image/webp";
|
|
197
|
+
if (header.startsWith("JVBER")) return "application/pdf";
|
|
198
|
+
return "application/octet-stream";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/relay/client.ts
|
|
168
202
|
var JUNIS_WS = (() => {
|
|
169
203
|
if (process.env.JUNIS_WS_URL) return process.env.JUNIS_WS_URL;
|
|
170
204
|
const apiUrl = process.env.JUNIS_API_URL ?? "https://junis.ai";
|
|
@@ -186,6 +220,8 @@ var RelayClient = class {
|
|
|
186
220
|
heartbeatTimer = null;
|
|
187
221
|
destroyed = false;
|
|
188
222
|
lastPongTime = 0;
|
|
223
|
+
// upload_url_response 대기용 pending 맵
|
|
224
|
+
pendingUploadRequests = /* @__PURE__ */ new Map();
|
|
189
225
|
async connect() {
|
|
190
226
|
if (this.destroyed) return;
|
|
191
227
|
const url = `${JUNIS_WS}/ws/devices/${this.config.device_key}`;
|
|
@@ -209,9 +245,22 @@ var RelayClient = class {
|
|
|
209
245
|
this.lastPongTime = Date.now();
|
|
210
246
|
return;
|
|
211
247
|
}
|
|
248
|
+
if (msg.type === "upload_url_response") {
|
|
249
|
+
const pending = this.pendingUploadRequests.get(msg.request_id);
|
|
250
|
+
if (pending) {
|
|
251
|
+
this.pendingUploadRequests.delete(msg.request_id);
|
|
252
|
+
if (msg.error) {
|
|
253
|
+
pending.reject(new Error(msg.error));
|
|
254
|
+
} else {
|
|
255
|
+
pending.resolve(msg);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
212
260
|
if (msg.type === "mcp_request") {
|
|
213
261
|
try {
|
|
214
|
-
|
|
262
|
+
let result = await this.onMCPRequest(msg.id, msg.payload);
|
|
263
|
+
result = await this.processLargeFiles(result);
|
|
215
264
|
this.send({ type: "mcp_response", id: msg.id, payload: result });
|
|
216
265
|
} catch (err) {
|
|
217
266
|
this.send({
|
|
@@ -266,6 +315,76 @@ var RelayClient = class {
|
|
|
266
315
|
this.ws.send(JSON.stringify(data));
|
|
267
316
|
}
|
|
268
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* 서버에 presigned PUT URL 요청.
|
|
320
|
+
* WebSocket으로 upload_url_request 전송 → upload_url_response 대기.
|
|
321
|
+
*/
|
|
322
|
+
requestUploadUrl(filename, contentType, size) {
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
324
|
+
const requestId = crypto.randomUUID();
|
|
325
|
+
const timeout = setTimeout(() => {
|
|
326
|
+
this.pendingUploadRequests.delete(requestId);
|
|
327
|
+
reject(new Error("Upload URL request timeout (30s)"));
|
|
328
|
+
}, 3e4);
|
|
329
|
+
this.pendingUploadRequests.set(requestId, {
|
|
330
|
+
resolve: (data) => {
|
|
331
|
+
clearTimeout(timeout);
|
|
332
|
+
resolve(data);
|
|
333
|
+
},
|
|
334
|
+
reject: (err) => {
|
|
335
|
+
clearTimeout(timeout);
|
|
336
|
+
reject(err);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
this.send({
|
|
340
|
+
type: "upload_url_request",
|
|
341
|
+
request_id: requestId,
|
|
342
|
+
filename,
|
|
343
|
+
content_type: contentType,
|
|
344
|
+
size
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* MCP 응답 내 대용량 base64 데이터를 감지하여 presigned URL 업로드 후 URL로 교체.
|
|
350
|
+
*
|
|
351
|
+
* 대상:
|
|
352
|
+
* 1. ImageContent: { type: "image", data: "<base64>", mimeType: "image/png" }
|
|
353
|
+
* → { type: "text", text: "" }
|
|
354
|
+
* 2. TextContent with large base64: { type: "text", text: "<huge base64>" }
|
|
355
|
+
* → { type: "text", text: "https://...access_url" }
|
|
356
|
+
*/
|
|
357
|
+
async processLargeFiles(result) {
|
|
358
|
+
if (!result || typeof result !== "object") return result;
|
|
359
|
+
const obj = result;
|
|
360
|
+
const inner = obj.result ?? obj;
|
|
361
|
+
const content = inner.content;
|
|
362
|
+
if (!Array.isArray(content)) return result;
|
|
363
|
+
for (let i = 0; i < content.length; i++) {
|
|
364
|
+
const item = content[i];
|
|
365
|
+
if (!item || typeof item !== "object") continue;
|
|
366
|
+
if (item.type === "image" && typeof item.data === "string" && isLargeBase64(item.data)) {
|
|
367
|
+
try {
|
|
368
|
+
const mimeType = item.mimeType || "image/png";
|
|
369
|
+
const ext = mimeType.split("/")[1] || "bin";
|
|
370
|
+
const url = await uploadLargeFile(this, item.data, `screenshot.${ext}`, mimeType);
|
|
371
|
+
content[i] = { type: "text", text: `` };
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.error("Failed to upload large image:", err);
|
|
374
|
+
}
|
|
375
|
+
} else if (item.type === "text" && typeof item.text === "string" && isLargeBase64(item.text) && /^[A-Za-z0-9+/\n\r]+=*$/.test(item.text.trim())) {
|
|
376
|
+
try {
|
|
377
|
+
const contentType = detectContentType(item.text);
|
|
378
|
+
const ext = contentType.split("/")[1] || "bin";
|
|
379
|
+
const url = await uploadLargeFile(this, item.text, `file.${ext}`, contentType);
|
|
380
|
+
content[i] = { type: "text", text: url };
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error("Failed to upload large text base64:", err);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
269
388
|
startHeartbeat() {
|
|
270
389
|
this.heartbeatTimer = setInterval(() => {
|
|
271
390
|
if (Date.now() - this.lastPongTime > 9e4) {
|
|
@@ -286,6 +405,10 @@ var RelayClient = class {
|
|
|
286
405
|
this.destroyed = true;
|
|
287
406
|
this.stopHeartbeat();
|
|
288
407
|
this.ws?.close();
|
|
408
|
+
for (const [, pending] of this.pendingUploadRequests) {
|
|
409
|
+
pending.reject(new Error("Client destroyed"));
|
|
410
|
+
}
|
|
411
|
+
this.pendingUploadRequests.clear();
|
|
289
412
|
}
|
|
290
413
|
};
|
|
291
414
|
|
|
@@ -338,9 +461,9 @@ var toolPermissions = {
|
|
|
338
461
|
cron_delete: "confirm",
|
|
339
462
|
edit_block: "confirm",
|
|
340
463
|
kill_process: "confirm",
|
|
341
|
-
// 시스템 변경 —
|
|
342
|
-
execute_command: "
|
|
343
|
-
write_file: "
|
|
464
|
+
// 시스템 변경 — 대화 기반 승인 (confirm)
|
|
465
|
+
execute_command: "confirm",
|
|
466
|
+
write_file: "confirm"
|
|
344
467
|
};
|
|
345
468
|
function checkPermission(toolName) {
|
|
346
469
|
const level = toolPermissions[toolName];
|
|
@@ -366,17 +489,15 @@ var FilesystemTools = class {
|
|
|
366
489
|
"- For reading files prefer read_file, for editing prefer edit_block, for searching prefer search_code.",
|
|
367
490
|
"",
|
|
368
491
|
"BEHAVIOR:",
|
|
369
|
-
"-
|
|
370
|
-
"- Destructive or irreversible commands (rm -rf, sudo, shutdown, mkfs): explain what will happen and get user confirmation first.",
|
|
492
|
+
"- Execute commands directly when the user requests them. Do not ask for confirmation \u2014 the user has already decided.",
|
|
371
493
|
"- If a command fails, analyze the error and suggest an alternative. Do not retry the identical command more than twice.",
|
|
372
494
|
"",
|
|
373
495
|
"SAFETY:",
|
|
374
|
-
"- Commands run with the user's full permissions.
|
|
375
|
-
"- Avoid piping untrusted input into shells. Use absolute paths when possible. Quote paths containing spaces."
|
|
496
|
+
"- Commands run with the user's full permissions. Use absolute paths when possible. Quote paths containing spaces."
|
|
376
497
|
].join("\n"),
|
|
377
498
|
{
|
|
378
499
|
command: z.string().describe("The shell command to execute. Use absolute paths when possible. Quote paths containing spaces."),
|
|
379
|
-
timeout_ms: z.number().optional().default(
|
|
500
|
+
timeout_ms: z.number().optional().default(12e4).describe("Maximum execution time in milliseconds (default: 120000). Increase for very long-running builds or downloads."),
|
|
380
501
|
background: z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
|
|
381
502
|
},
|
|
382
503
|
async ({ command, timeout_ms, background }) => {
|
|
@@ -863,11 +984,11 @@ var BrowserTools = class {
|
|
|
863
984
|
headless: z2.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
|
|
864
985
|
cdpUrl: z2.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
|
|
865
986
|
profile: z2.string().optional().describe("Browser profile name for persistent sessions \u2014 preserves cookies, logins, and history across restarts (managed mode only)"),
|
|
866
|
-
allowInternal: z2.boolean().optional().default(
|
|
987
|
+
allowInternal: z2.boolean().optional().default(true).describe("Allow navigation to localhost and internal network URLs (default: true for local agent)")
|
|
867
988
|
},
|
|
868
989
|
({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
|
|
869
990
|
if (this.browser) {
|
|
870
|
-
|
|
991
|
+
await this.cleanup();
|
|
871
992
|
}
|
|
872
993
|
if (mode === "remote-cdp") {
|
|
873
994
|
if (!cdpUrl) throw new Error("cdpUrl is required for remote-cdp mode");
|
|
@@ -1404,7 +1525,11 @@ var DeviceTools = class {
|
|
|
1404
1525
|
"Capture a photo from the device's camera and return it as base64 image data.",
|
|
1405
1526
|
"",
|
|
1406
1527
|
"Platform-specific: macOS (imagesnap), Windows (ffmpeg/dshow), Linux (fswebcam).",
|
|
1407
|
-
"
|
|
1528
|
+
"If output_path is provided, the file is also saved to disk.",
|
|
1529
|
+
"",
|
|
1530
|
+
"PERMISSIONS (macOS): Camera permission is needed. If it fails, macOS may show a native Allow/Deny dialog \u2014 ask the user to click Allow.",
|
|
1531
|
+
"If still denied, use execute_command to open Camera settings:",
|
|
1532
|
+
" open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Camera'"
|
|
1408
1533
|
].join("\n"),
|
|
1409
1534
|
{
|
|
1410
1535
|
output_path: z4.string().optional().describe("File path to save the captured photo. If omitted, returns image data only (temp file auto-cleaned).")
|
|
@@ -1422,11 +1547,10 @@ var DeviceTools = class {
|
|
|
1422
1547
|
await execAsync3(cmd);
|
|
1423
1548
|
} catch (err) {
|
|
1424
1549
|
const e = err;
|
|
1550
|
+
const hint = p === "mac" ? "\n\n\u{1F527} FIX: Camera permission may be needed. Try:\n1. Retry \u2014 macOS may show a native Allow/Deny dialog.\n2. If denied, run via execute_command: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Camera'\nAsk the user to toggle ON for 'imagesnap' (or their terminal app), then retry." : "";
|
|
1425
1551
|
return {
|
|
1426
|
-
content: [{ type: "text", text: `\u274C Camera
|
|
1427
|
-
Cause: ${e.message}
|
|
1428
|
-
|
|
1429
|
-
Please check if a camera is connected.` }],
|
|
1552
|
+
content: [{ type: "text", text: `\u274C Camera capture failed.
|
|
1553
|
+
Cause: ${e.message}${hint}` }],
|
|
1430
1554
|
isError: true
|
|
1431
1555
|
};
|
|
1432
1556
|
}
|
|
@@ -1625,6 +1749,13 @@ var APP_BLACKLIST = /* @__PURE__ */ new Set([
|
|
|
1625
1749
|
]);
|
|
1626
1750
|
var consecutiveFailures = 0;
|
|
1627
1751
|
var MAX_CONSECUTIVE_FAILURES = 2;
|
|
1752
|
+
var PERM_FIX_HINT = [
|
|
1753
|
+
"\n\n\u{1F527} PERMISSION FIX \u2014 run these via execute_command:",
|
|
1754
|
+
"1. Check status: peekaboo permissions --json-output",
|
|
1755
|
+
"2. Screen Recording: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'",
|
|
1756
|
+
"3. Accessibility: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility'",
|
|
1757
|
+
"Toggle ON for 'peekaboo' in the opened panel, then retry."
|
|
1758
|
+
].join("\n");
|
|
1628
1759
|
async function peekaboo(args) {
|
|
1629
1760
|
try {
|
|
1630
1761
|
const { stdout } = await execa("peekaboo", [...args, "--json-output"]);
|
|
@@ -1632,11 +1763,14 @@ async function peekaboo(args) {
|
|
|
1632
1763
|
return JSON.parse(stdout);
|
|
1633
1764
|
} catch (err) {
|
|
1634
1765
|
consecutiveFailures++;
|
|
1766
|
+
const msg = err.message?.toLowerCase() ?? "";
|
|
1767
|
+
const isPermError = msg.includes("permission") || msg.includes("accessibility") || msg.includes("screen recording") || msg.includes("not trusted") || msg.includes("not allowed") || msg.includes("denied");
|
|
1768
|
+
const hint = isPermError ? PERM_FIX_HINT : "";
|
|
1635
1769
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
1636
1770
|
consecutiveFailures = 0;
|
|
1637
|
-
throw new Error(`peekaboo failed ${MAX_CONSECUTIVE_FAILURES} times in a row. Auto-stopped for safety. Last error: ${err.message}`);
|
|
1771
|
+
throw new Error(`peekaboo failed ${MAX_CONSECUTIVE_FAILURES} times in a row. Auto-stopped for safety. Last error: ${err.message}${hint}`);
|
|
1638
1772
|
}
|
|
1639
|
-
throw err;
|
|
1773
|
+
throw new Error(`${err.message}${hint}`);
|
|
1640
1774
|
}
|
|
1641
1775
|
}
|
|
1642
1776
|
function checkBlacklist(app) {
|
|
@@ -1654,6 +1788,13 @@ var DesktopTools = class {
|
|
|
1654
1788
|
"WORKFLOW: List running apps \u2192 capture accessibility tree \u2192 find target element by role/label \u2192 interact using element ID or label (click, type, scroll).",
|
|
1655
1789
|
"Pass the returned snapshotId to subsequent interaction calls for 240x speed improvement (cached lookup vs. full re-scan).",
|
|
1656
1790
|
"",
|
|
1791
|
+
"PERMISSIONS: Desktop tools require macOS Accessibility + Screen Recording permissions for 'peekaboo'.",
|
|
1792
|
+
"If a tool fails with permission error, use execute_command to:",
|
|
1793
|
+
" 1. peekaboo permissions --json-output (check which are missing)",
|
|
1794
|
+
" 2. open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility'",
|
|
1795
|
+
" 3. open 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'",
|
|
1796
|
+
"Ask the user to toggle ON for 'peekaboo', then retry.",
|
|
1797
|
+
"",
|
|
1657
1798
|
"SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger an automatic safety stop."
|
|
1658
1799
|
].join("\n"),
|
|
1659
1800
|
{
|
package/dist/server/mcp.js
CHANGED
|
@@ -47,9 +47,9 @@ var toolPermissions = {
|
|
|
47
47
|
cron_delete: "confirm",
|
|
48
48
|
edit_block: "confirm",
|
|
49
49
|
kill_process: "confirm",
|
|
50
|
-
// 시스템 변경 —
|
|
51
|
-
execute_command: "
|
|
52
|
-
write_file: "
|
|
50
|
+
// 시스템 변경 — 대화 기반 승인 (confirm)
|
|
51
|
+
execute_command: "confirm",
|
|
52
|
+
write_file: "confirm"
|
|
53
53
|
};
|
|
54
54
|
function checkPermission(toolName) {
|
|
55
55
|
const level = toolPermissions[toolName];
|
|
@@ -75,17 +75,15 @@ var FilesystemTools = class {
|
|
|
75
75
|
"- For reading files prefer read_file, for editing prefer edit_block, for searching prefer search_code.",
|
|
76
76
|
"",
|
|
77
77
|
"BEHAVIOR:",
|
|
78
|
-
"-
|
|
79
|
-
"- Destructive or irreversible commands (rm -rf, sudo, shutdown, mkfs): explain what will happen and get user confirmation first.",
|
|
78
|
+
"- Execute commands directly when the user requests them. Do not ask for confirmation \u2014 the user has already decided.",
|
|
80
79
|
"- If a command fails, analyze the error and suggest an alternative. Do not retry the identical command more than twice.",
|
|
81
80
|
"",
|
|
82
81
|
"SAFETY:",
|
|
83
|
-
"- Commands run with the user's full permissions.
|
|
84
|
-
"- Avoid piping untrusted input into shells. Use absolute paths when possible. Quote paths containing spaces."
|
|
82
|
+
"- Commands run with the user's full permissions. Use absolute paths when possible. Quote paths containing spaces."
|
|
85
83
|
].join("\n"),
|
|
86
84
|
{
|
|
87
85
|
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(
|
|
86
|
+
timeout_ms: z.number().optional().default(12e4).describe("Maximum execution time in milliseconds (default: 120000). Increase for very long-running builds or downloads."),
|
|
89
87
|
background: z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
|
|
90
88
|
},
|
|
91
89
|
async ({ command, timeout_ms, background }) => {
|
|
@@ -572,11 +570,11 @@ var BrowserTools = class {
|
|
|
572
570
|
headless: z2.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
|
|
573
571
|
cdpUrl: z2.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
|
|
574
572
|
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(
|
|
573
|
+
allowInternal: z2.boolean().optional().default(true).describe("Allow navigation to localhost and internal network URLs (default: true for local agent)")
|
|
576
574
|
},
|
|
577
575
|
({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
|
|
578
576
|
if (this.browser) {
|
|
579
|
-
|
|
577
|
+
await this.cleanup();
|
|
580
578
|
}
|
|
581
579
|
if (mode === "remote-cdp") {
|
|
582
580
|
if (!cdpUrl) throw new Error("cdpUrl is required for remote-cdp mode");
|
|
@@ -1113,7 +1111,11 @@ var DeviceTools = class {
|
|
|
1113
1111
|
"Capture a photo from the device's camera and return it as base64 image data.",
|
|
1114
1112
|
"",
|
|
1115
1113
|
"Platform-specific: macOS (imagesnap), Windows (ffmpeg/dshow), Linux (fswebcam).",
|
|
1116
|
-
"
|
|
1114
|
+
"If output_path is provided, the file is also saved to disk.",
|
|
1115
|
+
"",
|
|
1116
|
+
"PERMISSIONS (macOS): Camera permission is needed. If it fails, macOS may show a native Allow/Deny dialog \u2014 ask the user to click Allow.",
|
|
1117
|
+
"If still denied, use execute_command to open Camera settings:",
|
|
1118
|
+
" open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Camera'"
|
|
1117
1119
|
].join("\n"),
|
|
1118
1120
|
{
|
|
1119
1121
|
output_path: z4.string().optional().describe("File path to save the captured photo. If omitted, returns image data only (temp file auto-cleaned).")
|
|
@@ -1131,11 +1133,10 @@ var DeviceTools = class {
|
|
|
1131
1133
|
await execAsync3(cmd);
|
|
1132
1134
|
} catch (err) {
|
|
1133
1135
|
const e = err;
|
|
1136
|
+
const hint = p === "mac" ? "\n\n\u{1F527} FIX: Camera permission may be needed. Try:\n1. Retry \u2014 macOS may show a native Allow/Deny dialog.\n2. If denied, run via execute_command: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Camera'\nAsk the user to toggle ON for 'imagesnap' (or their terminal app), then retry." : "";
|
|
1134
1137
|
return {
|
|
1135
|
-
content: [{ type: "text", text: `\u274C Camera
|
|
1136
|
-
Cause: ${e.message}
|
|
1137
|
-
|
|
1138
|
-
Please check if a camera is connected.` }],
|
|
1138
|
+
content: [{ type: "text", text: `\u274C Camera capture failed.
|
|
1139
|
+
Cause: ${e.message}${hint}` }],
|
|
1139
1140
|
isError: true
|
|
1140
1141
|
};
|
|
1141
1142
|
}
|
|
@@ -1334,6 +1335,13 @@ var APP_BLACKLIST = /* @__PURE__ */ new Set([
|
|
|
1334
1335
|
]);
|
|
1335
1336
|
var consecutiveFailures = 0;
|
|
1336
1337
|
var MAX_CONSECUTIVE_FAILURES = 2;
|
|
1338
|
+
var PERM_FIX_HINT = [
|
|
1339
|
+
"\n\n\u{1F527} PERMISSION FIX \u2014 run these via execute_command:",
|
|
1340
|
+
"1. Check status: peekaboo permissions --json-output",
|
|
1341
|
+
"2. Screen Recording: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'",
|
|
1342
|
+
"3. Accessibility: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility'",
|
|
1343
|
+
"Toggle ON for 'peekaboo' in the opened panel, then retry."
|
|
1344
|
+
].join("\n");
|
|
1337
1345
|
async function peekaboo(args) {
|
|
1338
1346
|
try {
|
|
1339
1347
|
const { stdout } = await execa("peekaboo", [...args, "--json-output"]);
|
|
@@ -1341,11 +1349,14 @@ async function peekaboo(args) {
|
|
|
1341
1349
|
return JSON.parse(stdout);
|
|
1342
1350
|
} catch (err) {
|
|
1343
1351
|
consecutiveFailures++;
|
|
1352
|
+
const msg = err.message?.toLowerCase() ?? "";
|
|
1353
|
+
const isPermError = msg.includes("permission") || msg.includes("accessibility") || msg.includes("screen recording") || msg.includes("not trusted") || msg.includes("not allowed") || msg.includes("denied");
|
|
1354
|
+
const hint = isPermError ? PERM_FIX_HINT : "";
|
|
1344
1355
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
1345
1356
|
consecutiveFailures = 0;
|
|
1346
|
-
throw new Error(`peekaboo failed ${MAX_CONSECUTIVE_FAILURES} times in a row. Auto-stopped for safety. Last error: ${err.message}`);
|
|
1357
|
+
throw new Error(`peekaboo failed ${MAX_CONSECUTIVE_FAILURES} times in a row. Auto-stopped for safety. Last error: ${err.message}${hint}`);
|
|
1347
1358
|
}
|
|
1348
|
-
throw err;
|
|
1359
|
+
throw new Error(`${err.message}${hint}`);
|
|
1349
1360
|
}
|
|
1350
1361
|
}
|
|
1351
1362
|
function checkBlacklist(app) {
|
|
@@ -1363,6 +1374,13 @@ var DesktopTools = class {
|
|
|
1363
1374
|
"WORKFLOW: List running apps \u2192 capture accessibility tree \u2192 find target element by role/label \u2192 interact using element ID or label (click, type, scroll).",
|
|
1364
1375
|
"Pass the returned snapshotId to subsequent interaction calls for 240x speed improvement (cached lookup vs. full re-scan).",
|
|
1365
1376
|
"",
|
|
1377
|
+
"PERMISSIONS: Desktop tools require macOS Accessibility + Screen Recording permissions for 'peekaboo'.",
|
|
1378
|
+
"If a tool fails with permission error, use execute_command to:",
|
|
1379
|
+
" 1. peekaboo permissions --json-output (check which are missing)",
|
|
1380
|
+
" 2. open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility'",
|
|
1381
|
+
" 3. open 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'",
|
|
1382
|
+
"Ask the user to toggle ON for 'peekaboo', then retry.",
|
|
1383
|
+
"",
|
|
1366
1384
|
"SAFETY: Terminal, iTerm, and Finder are blocked. Two consecutive failures trigger an automatic safety stop."
|
|
1367
1385
|
].join("\n"),
|
|
1368
1386
|
{
|
package/dist/server/stdio.js
CHANGED
|
@@ -48,9 +48,9 @@ var toolPermissions = {
|
|
|
48
48
|
cron_delete: "confirm",
|
|
49
49
|
edit_block: "confirm",
|
|
50
50
|
kill_process: "confirm",
|
|
51
|
-
// 시스템 변경 —
|
|
52
|
-
execute_command: "
|
|
53
|
-
write_file: "
|
|
51
|
+
// 시스템 변경 — 대화 기반 승인 (confirm)
|
|
52
|
+
execute_command: "confirm",
|
|
53
|
+
write_file: "confirm"
|
|
54
54
|
};
|
|
55
55
|
function checkPermission(toolName) {
|
|
56
56
|
const level = toolPermissions[toolName];
|
|
@@ -76,17 +76,15 @@ var FilesystemTools = class {
|
|
|
76
76
|
"- For reading files prefer read_file, for editing prefer edit_block, for searching prefer search_code.",
|
|
77
77
|
"",
|
|
78
78
|
"BEHAVIOR:",
|
|
79
|
-
"-
|
|
80
|
-
"- Destructive or irreversible commands (rm -rf, sudo, shutdown, mkfs): explain what will happen and get user confirmation first.",
|
|
79
|
+
"- Execute commands directly when the user requests them. Do not ask for confirmation \u2014 the user has already decided.",
|
|
81
80
|
"- If a command fails, analyze the error and suggest an alternative. Do not retry the identical command more than twice.",
|
|
82
81
|
"",
|
|
83
82
|
"SAFETY:",
|
|
84
|
-
"- Commands run with the user's full permissions.
|
|
85
|
-
"- Avoid piping untrusted input into shells. Use absolute paths when possible. Quote paths containing spaces."
|
|
83
|
+
"- Commands run with the user's full permissions. Use absolute paths when possible. Quote paths containing spaces."
|
|
86
84
|
].join("\n"),
|
|
87
85
|
{
|
|
88
86
|
command: z.string().describe("The shell command to execute. Use absolute paths when possible. Quote paths containing spaces."),
|
|
89
|
-
timeout_ms: z.number().optional().default(
|
|
87
|
+
timeout_ms: z.number().optional().default(12e4).describe("Maximum execution time in milliseconds (default: 120000). Increase for very long-running builds or downloads."),
|
|
90
88
|
background: z.boolean().optional().default(false).describe("Run in background without waiting for completion. Use for servers or long-running processes.")
|
|
91
89
|
},
|
|
92
90
|
async ({ command, timeout_ms, background }) => {
|
|
@@ -573,11 +571,11 @@ var BrowserTools = class {
|
|
|
573
571
|
headless: z2.boolean().optional().default(false).describe("Run without visible window (managed mode only). Use for background tasks."),
|
|
574
572
|
cdpUrl: z2.string().optional().describe("Chrome DevTools Protocol URL for remote-cdp mode (e.g. http://localhost:9222)"),
|
|
575
573
|
profile: z2.string().optional().describe("Browser profile name for persistent sessions \u2014 preserves cookies, logins, and history across restarts (managed mode only)"),
|
|
576
|
-
allowInternal: z2.boolean().optional().default(
|
|
574
|
+
allowInternal: z2.boolean().optional().default(true).describe("Allow navigation to localhost and internal network URLs (default: true for local agent)")
|
|
577
575
|
},
|
|
578
576
|
({ mode, headless, cdpUrl, profile, allowInternal }) => this.withLock(async () => {
|
|
579
577
|
if (this.browser) {
|
|
580
|
-
|
|
578
|
+
await this.cleanup();
|
|
581
579
|
}
|
|
582
580
|
if (mode === "remote-cdp") {
|
|
583
581
|
if (!cdpUrl) throw new Error("cdpUrl is required for remote-cdp mode");
|
|
@@ -1114,7 +1112,11 @@ var DeviceTools = class {
|
|
|
1114
1112
|
"Capture a photo from the device's camera and return it as base64 image data.",
|
|
1115
1113
|
"",
|
|
1116
1114
|
"Platform-specific: macOS (imagesnap), Windows (ffmpeg/dshow), Linux (fswebcam).",
|
|
1117
|
-
"
|
|
1115
|
+
"If output_path is provided, the file is also saved to disk.",
|
|
1116
|
+
"",
|
|
1117
|
+
"PERMISSIONS (macOS): Camera permission is needed. If it fails, macOS may show a native Allow/Deny dialog \u2014 ask the user to click Allow.",
|
|
1118
|
+
"If still denied, use execute_command to open Camera settings:",
|
|
1119
|
+
" open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Camera'"
|
|
1118
1120
|
].join("\n"),
|
|
1119
1121
|
{
|
|
1120
1122
|
output_path: z4.string().optional().describe("File path to save the captured photo. If omitted, returns image data only (temp file auto-cleaned).")
|
|
@@ -1132,11 +1134,10 @@ var DeviceTools = class {
|
|
|
1132
1134
|
await execAsync3(cmd);
|
|
1133
1135
|
} catch (err) {
|
|
1134
1136
|
const e = err;
|
|
1137
|
+
const hint = p === "mac" ? "\n\n\u{1F527} FIX: Camera permission may be needed. Try:\n1. Retry \u2014 macOS may show a native Allow/Deny dialog.\n2. If denied, run via execute_command: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_Camera'\nAsk the user to toggle ON for 'imagesnap' (or their terminal app), then retry." : "";
|
|
1135
1138
|
return {
|
|
1136
|
-
content: [{ type: "text", text: `\u274C Camera
|
|
1137
|
-
Cause: ${e.message}
|
|
1138
|
-
|
|
1139
|
-
Please check if a camera is connected.` }],
|
|
1139
|
+
content: [{ type: "text", text: `\u274C Camera capture failed.
|
|
1140
|
+
Cause: ${e.message}${hint}` }],
|
|
1140
1141
|
isError: true
|
|
1141
1142
|
};
|
|
1142
1143
|
}
|