@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 CHANGED
@@ -31,7 +31,7 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
31
31
 
32
32
  ```bash
33
33
  npm pack
34
- npm install -g ./leg3ndy-otto-bridge-0.2.0.tgz
34
+ npm install -g ./leg3ndy-otto-bridge-0.3.0.tgz
35
35
  ```
36
36
 
37
37
  ## Publicacao
@@ -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: "open_app", app });
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.runCommand("osascript", ["-e", `tell application "${escapeAppleScript(app)}" to activate`]);
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.runCommand("osascript", ["-e", `tell application "${escapeAppleScript(app)}" to activate`]);
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.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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",