@leg3ndy/otto-bridge 0.3.0 → 0.4.0
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/README.md +1 -1
- package/dist/executors/native_macos.js +220 -28
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, readdir, stat } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import process from "node:process";
|
|
3
6
|
import { JobCancelledError } from "./shared.js";
|
|
4
7
|
const KNOWN_APPS = [
|
|
@@ -17,6 +20,7 @@ const KNOWN_APPS = [
|
|
|
17
20
|
];
|
|
18
21
|
const KNOWN_SITES = [
|
|
19
22
|
{ url: "https://www.youtube.com", patterns: [/\byoutube\b/i, /\byou tube\b/i] },
|
|
23
|
+
{ url: "https://music.youtube.com", patterns: [/\byoutube music\b/i] },
|
|
20
24
|
{ url: "https://www.instagram.com", patterns: [/\binstagram\b/i, /\binstagram\.com\b/i] },
|
|
21
25
|
{ url: "https://mail.google.com", patterns: [/\bgmail\b/i] },
|
|
22
26
|
{ url: "https://www.google.com", patterns: [/\bgoogle\b/i] },
|
|
@@ -68,18 +72,77 @@ function normalizeUrl(raw) {
|
|
|
68
72
|
}
|
|
69
73
|
return `https://${trimmed.replace(/^\/+/, "")}`;
|
|
70
74
|
}
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
function expandUserPath(value) {
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
if (!trimmed) {
|
|
78
|
+
return os.homedir();
|
|
79
|
+
}
|
|
80
|
+
if (trimmed === "~") {
|
|
81
|
+
return os.homedir();
|
|
82
|
+
}
|
|
83
|
+
if (trimmed.startsWith("~/")) {
|
|
84
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
85
|
+
}
|
|
86
|
+
if (path.isAbsolute(trimmed)) {
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
89
|
+
return path.resolve(process.cwd(), trimmed);
|
|
90
|
+
}
|
|
91
|
+
function clipText(value, maxLength) {
|
|
92
|
+
if (value.length <= maxLength) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
return `${value.slice(0, maxLength)}...`;
|
|
96
|
+
}
|
|
97
|
+
function isSafeShellCommand(command) {
|
|
98
|
+
const trimmed = command.trim();
|
|
99
|
+
if (!trimmed) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const forbiddenPatterns = [
|
|
103
|
+
/(^|[;&|])\s*sudo\b/i,
|
|
104
|
+
/\brm\b/i,
|
|
105
|
+
/\bmv\b/i,
|
|
106
|
+
/\bcp\b/i,
|
|
107
|
+
/\bchmod\b/i,
|
|
108
|
+
/\bchown\b/i,
|
|
109
|
+
/\bshutdown\b/i,
|
|
110
|
+
/\breboot\b/i,
|
|
111
|
+
/\bmkfs\b/i,
|
|
112
|
+
/\bdd\b/i,
|
|
113
|
+
/\bkill(?:all)?\b/i,
|
|
114
|
+
/>/,
|
|
115
|
+
/>>/,
|
|
116
|
+
];
|
|
117
|
+
if (forbiddenPatterns.some((pattern) => pattern.test(trimmed))) {
|
|
118
|
+
return false;
|
|
81
119
|
}
|
|
82
|
-
|
|
120
|
+
const normalized = trimmed.replace(/\s+/g, " ");
|
|
121
|
+
const allowedPrefixes = [
|
|
122
|
+
"pwd",
|
|
123
|
+
"ls",
|
|
124
|
+
"cat ",
|
|
125
|
+
"cat",
|
|
126
|
+
"sed ",
|
|
127
|
+
"rg ",
|
|
128
|
+
"find ",
|
|
129
|
+
"git status",
|
|
130
|
+
"git log",
|
|
131
|
+
"git diff",
|
|
132
|
+
"head ",
|
|
133
|
+
"tail ",
|
|
134
|
+
"wc ",
|
|
135
|
+
"stat ",
|
|
136
|
+
"file ",
|
|
137
|
+
"mdls ",
|
|
138
|
+
"whoami",
|
|
139
|
+
"date",
|
|
140
|
+
"uname",
|
|
141
|
+
"python3 --version",
|
|
142
|
+
"node -v",
|
|
143
|
+
"npm -v",
|
|
144
|
+
];
|
|
145
|
+
return allowedPrefixes.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix} `));
|
|
83
146
|
}
|
|
84
147
|
function extractConfirmationOptions(job, actions) {
|
|
85
148
|
const payload = asRecord(job.payload);
|
|
@@ -169,6 +232,33 @@ function parseStructuredActions(job) {
|
|
|
169
232
|
if (shortcut) {
|
|
170
233
|
actions.push({ type: "press_shortcut", shortcut });
|
|
171
234
|
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (type === "take_screenshot" || type === "screenshot" || type === "screen_capture") {
|
|
238
|
+
const savePath = asString(action.path) || asString(action.save_path);
|
|
239
|
+
actions.push({ type: "take_screenshot", path: savePath || undefined });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (type === "read_file" || type === "read_local_file") {
|
|
243
|
+
const filePath = asString(action.path);
|
|
244
|
+
if (filePath) {
|
|
245
|
+
const maxChars = typeof action.max_chars === "number" ? Math.max(200, Math.min(12000, action.max_chars)) : undefined;
|
|
246
|
+
actions.push({ type: "read_file", path: filePath, max_chars: maxChars });
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (type === "list_files" || type === "ls") {
|
|
251
|
+
const filePath = asString(action.path) || "~";
|
|
252
|
+
const limit = typeof action.limit === "number" ? Math.max(1, Math.min(200, action.limit)) : undefined;
|
|
253
|
+
actions.push({ type: "list_files", path: filePath, limit });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (type === "run_shell" || type === "shell" || type === "terminal") {
|
|
257
|
+
const command = asString(action.command) || asString(action.cmd);
|
|
258
|
+
const cwd = asString(action.cwd);
|
|
259
|
+
if (command) {
|
|
260
|
+
actions.push({ type: "run_shell", command, cwd: cwd || undefined });
|
|
261
|
+
}
|
|
172
262
|
}
|
|
173
263
|
}
|
|
174
264
|
return actions;
|
|
@@ -222,6 +312,7 @@ export class NativeMacOSJobExecutor {
|
|
|
222
312
|
}
|
|
223
313
|
}
|
|
224
314
|
try {
|
|
315
|
+
const completionNotes = [];
|
|
225
316
|
for (let index = 0; index < actions.length; index += 1) {
|
|
226
317
|
this.assertNotCancelled(job.job_id);
|
|
227
318
|
const action = actions[index];
|
|
@@ -246,12 +337,38 @@ export class NativeMacOSJobExecutor {
|
|
|
246
337
|
await this.typeText(action.text);
|
|
247
338
|
continue;
|
|
248
339
|
}
|
|
340
|
+
if (action.type === "take_screenshot") {
|
|
341
|
+
await reporter.progress(progressPercent, "Capturando screenshot do Mac");
|
|
342
|
+
const screenshotPath = await this.takeScreenshot(action.path);
|
|
343
|
+
completionNotes.push(`Screenshot salvo em ${screenshotPath}`);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (action.type === "read_file") {
|
|
347
|
+
await reporter.progress(progressPercent, `Lendo ${action.path}`);
|
|
348
|
+
const fileContent = await this.readLocalFile(action.path, action.max_chars);
|
|
349
|
+
completionNotes.push(`Conteudo de ${action.path}:\n${fileContent}`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (action.type === "list_files") {
|
|
353
|
+
await reporter.progress(progressPercent, `Listando arquivos em ${action.path}`);
|
|
354
|
+
const listing = await this.listLocalFiles(action.path, action.limit);
|
|
355
|
+
completionNotes.push(`Arquivos em ${action.path}:\n${listing}`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (action.type === "run_shell") {
|
|
359
|
+
await reporter.progress(progressPercent, `Rodando comando local: ${action.command}`);
|
|
360
|
+
const shellOutput = await this.runShellCommand(action.command, action.cwd);
|
|
361
|
+
completionNotes.push(`Saida de \`${action.command}\`:\n${shellOutput}`);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
249
364
|
await reporter.progress(progressPercent, `Abrindo ${action.url}${action.app ? ` em ${action.app}` : ""}`);
|
|
250
365
|
await this.openUrl(action.url, action.app);
|
|
251
366
|
}
|
|
252
|
-
const summary =
|
|
253
|
-
?
|
|
254
|
-
:
|
|
367
|
+
const summary = completionNotes.length > 0
|
|
368
|
+
? completionNotes.join("\n\n")
|
|
369
|
+
: (actions.length === 1
|
|
370
|
+
? this.describeAction(actions[0])
|
|
371
|
+
: `${actions.length} ações executadas no macOS`);
|
|
255
372
|
await reporter.completed({
|
|
256
373
|
executor: "native-macos",
|
|
257
374
|
summary,
|
|
@@ -320,22 +437,70 @@ export class NativeMacOSJobExecutor {
|
|
|
320
437
|
]);
|
|
321
438
|
}
|
|
322
439
|
async typeText(text) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
440
|
+
const previousClipboard = await this.readClipboardText();
|
|
441
|
+
try {
|
|
442
|
+
await this.writeClipboardText(text);
|
|
443
|
+
await this.pressShortcut("cmd+v");
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
if (previousClipboard !== null) {
|
|
447
|
+
await this.writeClipboardText(previousClipboard).catch(() => undefined);
|
|
331
448
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async takeScreenshot(targetPath) {
|
|
452
|
+
const artifactsDir = path.join(os.homedir(), ".otto-bridge", "artifacts");
|
|
453
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
454
|
+
const screenshotPath = targetPath
|
|
455
|
+
? expandUserPath(targetPath)
|
|
456
|
+
: path.join(artifactsDir, `screenshot-${Date.now()}.png`);
|
|
457
|
+
await this.runCommand("screencapture", ["-x", screenshotPath]);
|
|
458
|
+
return screenshotPath;
|
|
459
|
+
}
|
|
460
|
+
async readLocalFile(filePath, maxChars = 4000) {
|
|
461
|
+
const resolved = expandUserPath(filePath);
|
|
462
|
+
const content = await readFile(resolved, "utf8");
|
|
463
|
+
return clipText(content.trim() || "(arquivo vazio)", maxChars);
|
|
464
|
+
}
|
|
465
|
+
async listLocalFiles(directoryPath, limit = 40) {
|
|
466
|
+
const resolved = expandUserPath(directoryPath);
|
|
467
|
+
const entries = await readdir(resolved, { withFileTypes: true });
|
|
468
|
+
const items = await Promise.all(entries.slice(0, limit).map(async (entry) => {
|
|
469
|
+
const entryPath = path.join(resolved, entry.name);
|
|
470
|
+
let suffix = "";
|
|
471
|
+
try {
|
|
472
|
+
const entryStat = await stat(entryPath);
|
|
473
|
+
suffix = entry.isDirectory() ? "/" : ` (${entryStat.size} bytes)`;
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
suffix = entry.isDirectory() ? "/" : "";
|
|
337
477
|
}
|
|
478
|
+
return `${entry.name}${suffix}`;
|
|
479
|
+
}));
|
|
480
|
+
return items.length > 0 ? items.join("\n") : "(pasta vazia)";
|
|
481
|
+
}
|
|
482
|
+
async runShellCommand(command, cwd) {
|
|
483
|
+
if (!isSafeShellCommand(command)) {
|
|
484
|
+
throw new Error("Otto Bridge permite apenas shell de consulta no momento. Use comandos de leitura como pwd, ls, cat, rg, find ou git status.");
|
|
338
485
|
}
|
|
486
|
+
const resolvedCwd = cwd ? expandUserPath(cwd) : process.cwd();
|
|
487
|
+
const { stdout, stderr } = await this.runCommandCapture("/bin/zsh", ["-lc", command], {
|
|
488
|
+
cwd: resolvedCwd,
|
|
489
|
+
});
|
|
490
|
+
const combined = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
|
|
491
|
+
return clipText(combined || "(sem saida)", 4000);
|
|
492
|
+
}
|
|
493
|
+
async readClipboardText() {
|
|
494
|
+
try {
|
|
495
|
+
const { stdout } = await this.runCommandCapture("pbpaste", []);
|
|
496
|
+
return stdout;
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async writeClipboardText(text) {
|
|
503
|
+
await this.runCommandCapture("pbcopy", [], { stdin: text });
|
|
339
504
|
}
|
|
340
505
|
describeAction(action) {
|
|
341
506
|
if (action.type === "open_app") {
|
|
@@ -350,17 +515,40 @@ export class NativeMacOSJobExecutor {
|
|
|
350
515
|
if (action.type === "type_text") {
|
|
351
516
|
return "Texto digitado no aplicativo ativo";
|
|
352
517
|
}
|
|
518
|
+
if (action.type === "take_screenshot") {
|
|
519
|
+
return "Screenshot capturado no macOS";
|
|
520
|
+
}
|
|
521
|
+
if (action.type === "read_file") {
|
|
522
|
+
return `${action.path} foi lido no macOS`;
|
|
523
|
+
}
|
|
524
|
+
if (action.type === "list_files") {
|
|
525
|
+
return `Arquivos listados em ${action.path}`;
|
|
526
|
+
}
|
|
527
|
+
if (action.type === "run_shell") {
|
|
528
|
+
return `Comando ${action.command} executado no macOS`;
|
|
529
|
+
}
|
|
353
530
|
return `${action.url} foi aberto${action.app ? ` em ${action.app}` : ""}`;
|
|
354
531
|
}
|
|
355
532
|
async runCommand(command, args) {
|
|
533
|
+
await this.runCommandCapture(command, args);
|
|
534
|
+
}
|
|
535
|
+
async runCommandCapture(command, args, options) {
|
|
356
536
|
const child = spawn(command, args, {
|
|
357
|
-
|
|
537
|
+
cwd: options?.cwd,
|
|
538
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
358
539
|
});
|
|
359
540
|
this.activeChild = child;
|
|
360
541
|
try {
|
|
361
542
|
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
362
543
|
let stdout = "";
|
|
363
544
|
let stderr = "";
|
|
545
|
+
if (options?.stdin !== undefined) {
|
|
546
|
+
child.stdin.write(options.stdin);
|
|
547
|
+
child.stdin.end();
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
child.stdin.end();
|
|
551
|
+
}
|
|
364
552
|
child.stdout.on("data", (chunk) => {
|
|
365
553
|
stdout += String(chunk);
|
|
366
554
|
});
|
|
@@ -386,9 +574,13 @@ export class NativeMacOSJobExecutor {
|
|
|
386
574
|
if (stdoutText) {
|
|
387
575
|
console.log(`[otto-bridge] ${command} stdout=${stdoutText}`);
|
|
388
576
|
}
|
|
577
|
+
return { stdout, stderr };
|
|
389
578
|
}
|
|
390
579
|
catch (error) {
|
|
391
580
|
const detail = error instanceof Error ? error.message : String(error);
|
|
581
|
+
if (detail.includes("System Events") || detail.includes("(1002)")) {
|
|
582
|
+
throw new Error("macOS bloqueou o controle de teclado do Otto Bridge. Abra Ajustes do Sistema > Privacidade e Seguranca > Acessibilidade e autorize o terminal ou app que esta rodando o otto-bridge.");
|
|
583
|
+
}
|
|
392
584
|
throw new Error(detail);
|
|
393
585
|
}
|
|
394
586
|
finally {
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.
|
|
2
|
+
export const BRIDGE_VERSION = "0.4.0";
|
|
3
3
|
export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
|
|
4
4
|
export const DEFAULT_API_BASE_URL = "http://localhost:8000";
|
|
5
5
|
export const DEFAULT_POLL_INTERVAL_MS = 3000;
|