@leg3ndy/otto-bridge 0.2.0 → 0.3.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 +148 -3
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,10 @@ const KNOWN_APPS = [
|
|
|
5
5
|
{ canonical: "Safari", patterns: [/\bsafari\b/i] },
|
|
6
6
|
{ canonical: "Google Chrome", patterns: [/\bgoogle chrome\b/i, /\bchrome\b/i] },
|
|
7
7
|
{ canonical: "Firefox", patterns: [/\bfirefox\b/i] },
|
|
8
|
+
{ canonical: "Spotify", patterns: [/\bspotify\b/i, /\bspofity\b/i] },
|
|
8
9
|
{ canonical: "Finder", patterns: [/\bfinder\b/i] },
|
|
9
10
|
{ canonical: "Notes", patterns: [/\bnotes\b/i, /\bnotas\b/i] },
|
|
11
|
+
{ canonical: "TextEdit", patterns: [/\btextedit\b/i, /\bbloco de notas\b/i, /\beditor de texto\b/i] },
|
|
10
12
|
{ canonical: "Mail", patterns: [/\bmail\b/i] },
|
|
11
13
|
{ canonical: "Messages", patterns: [/\bmessages\b/i, /\bmensagens\b/i] },
|
|
12
14
|
{ canonical: "Calendar", patterns: [/\bcalendar\b/i, /\bcalend[áa]rio\b/i] },
|
|
@@ -15,6 +17,7 @@ const KNOWN_APPS = [
|
|
|
15
17
|
];
|
|
16
18
|
const KNOWN_SITES = [
|
|
17
19
|
{ url: "https://www.youtube.com", patterns: [/\byoutube\b/i, /\byou tube\b/i] },
|
|
20
|
+
{ url: "https://www.instagram.com", patterns: [/\binstagram\b/i, /\binstagram\.com\b/i] },
|
|
18
21
|
{ url: "https://mail.google.com", patterns: [/\bgmail\b/i] },
|
|
19
22
|
{ url: "https://www.google.com", patterns: [/\bgoogle\b/i] },
|
|
20
23
|
{ url: "https://github.com", patterns: [/\bgithub\b/i] },
|
|
@@ -65,6 +68,49 @@ function normalizeUrl(raw) {
|
|
|
65
68
|
}
|
|
66
69
|
return `https://${trimmed.replace(/^\/+/, "")}`;
|
|
67
70
|
}
|
|
71
|
+
function splitTextForTyping(value) {
|
|
72
|
+
const parts = [];
|
|
73
|
+
for (const line of value.split("\n")) {
|
|
74
|
+
if (line.length <= 220) {
|
|
75
|
+
parts.push(line);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (let index = 0; index < line.length; index += 220) {
|
|
79
|
+
parts.push(line.slice(index, index + 220));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return parts;
|
|
83
|
+
}
|
|
84
|
+
function extractConfirmationOptions(job, actions) {
|
|
85
|
+
const payload = asRecord(job.payload);
|
|
86
|
+
const requested = payload.requires_confirmation === true;
|
|
87
|
+
const longTyping = actions.some((action) => action.type === "type_text" && action.text.trim().length >= 180);
|
|
88
|
+
const confirmationMessage = asString(payload.confirmation_message)
|
|
89
|
+
|| `O Otto quer executar ${actions.length} ação${actions.length === 1 ? "" : "ões"} no seu Mac.`;
|
|
90
|
+
return {
|
|
91
|
+
required: requested || longTyping,
|
|
92
|
+
message: confirmationMessage,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parseShortcut(shortcut) {
|
|
96
|
+
const parts = shortcut
|
|
97
|
+
.split("+")
|
|
98
|
+
.map((part) => part.trim().toLowerCase())
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
const key = parts.pop() || "";
|
|
101
|
+
const modifiers = parts.map((part) => {
|
|
102
|
+
if (part === "cmd" || part === "command")
|
|
103
|
+
return "command down";
|
|
104
|
+
if (part === "shift")
|
|
105
|
+
return "shift down";
|
|
106
|
+
if (part === "alt" || part === "option")
|
|
107
|
+
return "option down";
|
|
108
|
+
if (part === "ctrl" || part === "control")
|
|
109
|
+
return "control down";
|
|
110
|
+
return "";
|
|
111
|
+
}).filter(Boolean);
|
|
112
|
+
return { key, modifiers };
|
|
113
|
+
}
|
|
68
114
|
function detectKnownApp(task) {
|
|
69
115
|
for (const app of KNOWN_APPS) {
|
|
70
116
|
if (app.patterns.some((pattern) => pattern.test(task))) {
|
|
@@ -99,7 +145,7 @@ function parseStructuredActions(job) {
|
|
|
99
145
|
if (type === "open_app" || type === "focus_app") {
|
|
100
146
|
const app = asString(action.app) || asString(action.application) || asString(action.name);
|
|
101
147
|
if (app) {
|
|
102
|
-
actions.push({ type
|
|
148
|
+
actions.push({ type, app });
|
|
103
149
|
}
|
|
104
150
|
continue;
|
|
105
151
|
}
|
|
@@ -109,6 +155,20 @@ function parseStructuredActions(job) {
|
|
|
109
155
|
if (url) {
|
|
110
156
|
actions.push({ type: "open_url", url: normalizeUrl(url), app: app || undefined });
|
|
111
157
|
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (type === "type_text" || type === "write_text" || type === "keystroke") {
|
|
161
|
+
const text = asString(action.text) || asString(action.content);
|
|
162
|
+
if (text) {
|
|
163
|
+
actions.push({ type: "type_text", text });
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (type === "press_shortcut" || type === "shortcut") {
|
|
168
|
+
const shortcut = asString(action.shortcut) || asString(action.keys);
|
|
169
|
+
if (shortcut) {
|
|
170
|
+
actions.push({ type: "press_shortcut", shortcut });
|
|
171
|
+
}
|
|
112
172
|
}
|
|
113
173
|
}
|
|
114
174
|
return actions;
|
|
@@ -151,6 +211,16 @@ export class NativeMacOSJobExecutor {
|
|
|
151
211
|
throw new Error("Otto Bridge native-macos could not derive a supported local action from this request");
|
|
152
212
|
}
|
|
153
213
|
await reporter.accepted();
|
|
214
|
+
const confirmation = extractConfirmationOptions(job, actions);
|
|
215
|
+
if (confirmation.required) {
|
|
216
|
+
const decision = await reporter.confirmRequired(confirmation.message, {
|
|
217
|
+
actions,
|
|
218
|
+
executor: "native-macos",
|
|
219
|
+
});
|
|
220
|
+
if (decision.action !== "approve") {
|
|
221
|
+
throw new JobCancelledError(job.job_id);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
154
224
|
try {
|
|
155
225
|
for (let index = 0; index < actions.length; index += 1) {
|
|
156
226
|
this.assertNotCancelled(job.job_id);
|
|
@@ -161,6 +231,21 @@ export class NativeMacOSJobExecutor {
|
|
|
161
231
|
await this.openApp(action.app);
|
|
162
232
|
continue;
|
|
163
233
|
}
|
|
234
|
+
if (action.type === "focus_app") {
|
|
235
|
+
await reporter.progress(progressPercent, `Trazendo ${action.app} para frente`);
|
|
236
|
+
await this.focusApp(action.app);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (action.type === "press_shortcut") {
|
|
240
|
+
await reporter.progress(progressPercent, `Enviando atalho ${action.shortcut}`);
|
|
241
|
+
await this.pressShortcut(action.shortcut);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (action.type === "type_text") {
|
|
245
|
+
await reporter.progress(progressPercent, "Digitando texto no app ativo");
|
|
246
|
+
await this.typeText(action.text);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
164
249
|
await reporter.progress(progressPercent, `Abrindo ${action.url}${action.app ? ` em ${action.app}` : ""}`);
|
|
165
250
|
await this.openUrl(action.url, action.app);
|
|
166
251
|
}
|
|
@@ -191,20 +276,80 @@ export class NativeMacOSJobExecutor {
|
|
|
191
276
|
}
|
|
192
277
|
async openApp(app) {
|
|
193
278
|
await this.runCommand("open", ["-a", app]);
|
|
194
|
-
await this.
|
|
279
|
+
await this.focusApp(app);
|
|
195
280
|
}
|
|
196
281
|
async openUrl(url, app) {
|
|
197
282
|
if (app) {
|
|
198
283
|
await this.runCommand("open", ["-a", app, url]);
|
|
199
|
-
await this.
|
|
284
|
+
await this.focusApp(app);
|
|
200
285
|
return;
|
|
201
286
|
}
|
|
202
287
|
await this.runCommand("open", [url]);
|
|
203
288
|
}
|
|
289
|
+
async focusApp(app) {
|
|
290
|
+
await this.runCommand("osascript", ["-e", `tell application "${escapeAppleScript(app)}" to activate`]);
|
|
291
|
+
}
|
|
292
|
+
async pressShortcut(shortcut) {
|
|
293
|
+
const { key, modifiers } = parseShortcut(shortcut);
|
|
294
|
+
if (!key) {
|
|
295
|
+
throw new Error(`Invalid shortcut: ${shortcut}`);
|
|
296
|
+
}
|
|
297
|
+
const namedKeyCodes = {
|
|
298
|
+
return: 36,
|
|
299
|
+
enter: 36,
|
|
300
|
+
tab: 48,
|
|
301
|
+
space: 49,
|
|
302
|
+
escape: 53,
|
|
303
|
+
esc: 53,
|
|
304
|
+
left: 123,
|
|
305
|
+
right: 124,
|
|
306
|
+
down: 125,
|
|
307
|
+
up: 126,
|
|
308
|
+
};
|
|
309
|
+
const usingClause = modifiers.length > 0 ? ` using {${modifiers.join(", ")}}` : "";
|
|
310
|
+
if (namedKeyCodes[key] !== undefined) {
|
|
311
|
+
await this.runCommand("osascript", [
|
|
312
|
+
"-e",
|
|
313
|
+
`tell application "System Events" to key code ${namedKeyCodes[key]}${usingClause}`,
|
|
314
|
+
]);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
await this.runCommand("osascript", [
|
|
318
|
+
"-e",
|
|
319
|
+
`tell application "System Events" to keystroke "${escapeAppleScript(key)}"${usingClause}`,
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
async typeText(text) {
|
|
323
|
+
const chunks = splitTextForTyping(text);
|
|
324
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
325
|
+
const chunk = chunks[index];
|
|
326
|
+
if (chunk) {
|
|
327
|
+
await this.runCommand("osascript", [
|
|
328
|
+
"-e",
|
|
329
|
+
`tell application "System Events" to keystroke "${escapeAppleScript(chunk)}"`,
|
|
330
|
+
]);
|
|
331
|
+
}
|
|
332
|
+
if (index < chunks.length - 1) {
|
|
333
|
+
await this.runCommand("osascript", [
|
|
334
|
+
"-e",
|
|
335
|
+
'tell application "System Events" to key code 36',
|
|
336
|
+
]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
204
340
|
describeAction(action) {
|
|
205
341
|
if (action.type === "open_app") {
|
|
206
342
|
return `${action.app} foi aberto no macOS`;
|
|
207
343
|
}
|
|
344
|
+
if (action.type === "focus_app") {
|
|
345
|
+
return `${action.app} ficou em foco no macOS`;
|
|
346
|
+
}
|
|
347
|
+
if (action.type === "press_shortcut") {
|
|
348
|
+
return `Atalho ${action.shortcut} executado no macOS`;
|
|
349
|
+
}
|
|
350
|
+
if (action.type === "type_text") {
|
|
351
|
+
return "Texto digitado no aplicativo ativo";
|
|
352
|
+
}
|
|
208
353
|
return `${action.url} foi aberto${action.app ? ` em ${action.app}` : ""}`;
|
|
209
354
|
}
|
|
210
355
|
async runCommand(command, args) {
|
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.3.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;
|