@leg3ndy/otto-bridge 0.4.0 → 0.4.2

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.4.0.tgz
34
+ npm install -g ./leg3ndy-otto-bridge-0.4.1.tgz
35
35
  ```
36
36
 
37
37
  ## Publicacao
@@ -19,15 +19,15 @@ const KNOWN_APPS = [
19
19
  { canonical: "System Settings", patterns: [/\bsystem settings\b/i, /\bajustes do sistema\b/i, /\bconfigura[cç][õo]es do sistema\b/i] },
20
20
  ];
21
21
  const KNOWN_SITES = [
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] },
24
- { url: "https://www.instagram.com", patterns: [/\binstagram\b/i, /\binstagram\.com\b/i] },
25
- { url: "https://mail.google.com", patterns: [/\bgmail\b/i] },
26
- { url: "https://www.google.com", patterns: [/\bgoogle\b/i] },
27
- { url: "https://github.com", patterns: [/\bgithub\b/i] },
28
- { url: "https://chat.openai.com", patterns: [/\bchatgpt\b/i] },
29
- { url: "https://web.whatsapp.com", patterns: [/\bwhatsapp\b/i] },
30
- { url: "https://x.com", patterns: [/\bx\.com\b/i, /\btwitter\b/i, /\bxis\b/i] },
22
+ { label: "YouTube", url: "https://www.youtube.com", patterns: [/\byoutube\b/i, /\byou tube\b/i] },
23
+ { label: "YouTube Music", url: "https://music.youtube.com", patterns: [/\byoutube music\b/i] },
24
+ { label: "Instagram", url: "https://www.instagram.com", patterns: [/\binstagram\b/i, /\binstagram\.com\b/i] },
25
+ { label: "Gmail", url: "https://mail.google.com", patterns: [/\bgmail\b/i] },
26
+ { label: "Google", url: "https://www.google.com", patterns: [/\bgoogle\b/i] },
27
+ { label: "GitHub", url: "https://github.com", patterns: [/\bgithub\b/i] },
28
+ { label: "ChatGPT", url: "https://chat.openai.com", patterns: [/\bchatgpt\b/i] },
29
+ { label: "WhatsApp Web", url: "https://web.whatsapp.com", patterns: [/\bwhatsapp\b/i] },
30
+ { label: "X", url: "https://x.com", patterns: [/\bx\.com\b/i, /\btwitter\b/i, /\bxis\b/i] },
31
31
  ];
32
32
  function asRecord(value) {
33
33
  return value && typeof value === "object" ? value : {};
@@ -72,6 +72,21 @@ function normalizeUrl(raw) {
72
72
  }
73
73
  return `https://${trimmed.replace(/^\/+/, "")}`;
74
74
  }
75
+ function humanizeUrl(url) {
76
+ const normalized = normalizeUrl(url);
77
+ for (const site of KNOWN_SITES) {
78
+ if (normalizeUrl(site.url) === normalized) {
79
+ return site.label || site.url;
80
+ }
81
+ }
82
+ try {
83
+ const parsed = new URL(normalized);
84
+ return parsed.hostname.replace(/^www\./i, "");
85
+ }
86
+ catch {
87
+ return normalized;
88
+ }
89
+ }
75
90
  function expandUserPath(value) {
76
91
  const trimmed = value.trim();
77
92
  if (!trimmed) {
@@ -94,6 +109,47 @@ function clipText(value, maxLength) {
94
109
  }
95
110
  return `${value.slice(0, maxLength)}...`;
96
111
  }
112
+ function escapeHtml(value) {
113
+ return value
114
+ .replace(/&/g, "&")
115
+ .replace(/</g, "&lt;")
116
+ .replace(/>/g, "&gt;");
117
+ }
118
+ function deriveNoteTitle(text, fallback = "Nota Otto") {
119
+ const firstLine = text
120
+ .split(/\r?\n/)
121
+ .map((line) => line.trim().replace(/^#+\s*/, ""))
122
+ .find(Boolean);
123
+ return firstLine ? clipText(firstLine, 80) : fallback;
124
+ }
125
+ function normalizeNoteHeading(value) {
126
+ return value.trim().replace(/^#+\s*/, "");
127
+ }
128
+ function stripDuplicatedTitleFromText(text, title) {
129
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
130
+ if (!lines.length) {
131
+ return text;
132
+ }
133
+ const firstMeaningfulIndex = lines.findIndex((line) => line.trim().length > 0);
134
+ if (firstMeaningfulIndex < 0) {
135
+ return text;
136
+ }
137
+ const firstMeaningfulLine = normalizeNoteHeading(lines[firstMeaningfulIndex]);
138
+ if (firstMeaningfulLine !== normalizeNoteHeading(title)) {
139
+ return text;
140
+ }
141
+ const remaining = lines.slice(firstMeaningfulIndex + 1).join("\n").replace(/^\n+/, "").trim();
142
+ return remaining || text;
143
+ }
144
+ function noteBodyToHtml(text) {
145
+ const normalized = text.replace(/\r\n/g, "\n").trim();
146
+ const blocks = normalized
147
+ .split(/\n{2,}/)
148
+ .map((block) => block.trim())
149
+ .filter(Boolean)
150
+ .map((block) => `<p>${escapeHtml(block).replace(/\n/g, "<br>")}</p>`);
151
+ return blocks.join("");
152
+ }
97
153
  function isSafeShellCommand(command) {
98
154
  const trimmed = command.trim();
99
155
  if (!trimmed) {
@@ -220,6 +276,18 @@ function parseStructuredActions(job) {
220
276
  }
221
277
  continue;
222
278
  }
279
+ if (type === "create_note" || type === "write_note") {
280
+ const text = asString(action.text) || asString(action.body) || asString(action.content);
281
+ if (text) {
282
+ actions.push({
283
+ type: "create_note",
284
+ app: asString(action.app) || "Notes",
285
+ title: asString(action.title) || asString(action.name) || deriveNoteTitle(text),
286
+ text,
287
+ });
288
+ }
289
+ continue;
290
+ }
223
291
  if (type === "type_text" || type === "write_text" || type === "keystroke") {
224
292
  const text = asString(action.text) || asString(action.content);
225
293
  if (text) {
@@ -263,6 +331,32 @@ function parseStructuredActions(job) {
263
331
  }
264
332
  return actions;
265
333
  }
334
+ function collapseLegacyNotePlan(actions) {
335
+ if (!actions.length) {
336
+ return actions;
337
+ }
338
+ const hasOnlyLegacyNoteActions = actions.every((action) => (action.type === "open_app"
339
+ || action.type === "focus_app"
340
+ || action.type === "press_shortcut"
341
+ || action.type === "type_text"));
342
+ if (!hasOnlyLegacyNoteActions) {
343
+ return actions;
344
+ }
345
+ const opensNotes = actions.some((action) => ((action.type === "open_app" || action.type === "focus_app") && action.app === "Notes"));
346
+ const createsNewNote = actions.some((action) => action.type === "press_shortcut" && action.shortcut.toLowerCase() === "cmd+n");
347
+ const noteText = actions.find((action) => action.type === "type_text");
348
+ if (!opensNotes || !createsNewNote || !noteText) {
349
+ return actions;
350
+ }
351
+ return [
352
+ {
353
+ type: "create_note",
354
+ app: "Notes",
355
+ title: deriveNoteTitle(noteText.text),
356
+ text: noteText.text,
357
+ },
358
+ ];
359
+ }
266
360
  function deriveActionsFromText(job) {
267
361
  const task = extractTaskText(job);
268
362
  const detectedApp = detectKnownApp(task);
@@ -285,7 +379,7 @@ function deriveActionsFromText(job) {
285
379
  function extractActions(job) {
286
380
  const structured = parseStructuredActions(job);
287
381
  if (structured.length > 0) {
288
- return structured;
382
+ return collapseLegacyNotePlan(structured);
289
383
  }
290
384
  return deriveActionsFromText(job);
291
385
  }
@@ -332,6 +426,12 @@ export class NativeMacOSJobExecutor {
332
426
  await this.pressShortcut(action.shortcut);
333
427
  continue;
334
428
  }
429
+ if (action.type === "create_note") {
430
+ await reporter.progress(progressPercent, "Criando nota no Notes");
431
+ const noteTitle = await this.createNote(action.text, action.title);
432
+ completionNotes.push(`Nota criada no Notes: ${noteTitle}`);
433
+ continue;
434
+ }
335
435
  if (action.type === "type_text") {
336
436
  await reporter.progress(progressPercent, "Digitando texto no app ativo");
337
437
  await this.typeText(action.text);
@@ -403,6 +503,27 @@ export class NativeMacOSJobExecutor {
403
503
  }
404
504
  await this.runCommand("open", [url]);
405
505
  }
506
+ async createNote(text, title) {
507
+ const noteTitle = clipText((title || deriveNoteTitle(text)).trim() || "Nota Otto", 120);
508
+ const noteBodyText = stripDuplicatedTitleFromText(text, noteTitle);
509
+ const noteHtml = noteBodyToHtml(noteBodyText);
510
+ const script = `
511
+ set noteTitle to "${escapeAppleScript(noteTitle)}"
512
+ set noteBody to "${escapeAppleScript(noteHtml)}"
513
+ tell application "Notes"
514
+ activate
515
+ if not (exists default account) then error "Nenhuma conta padrão do Notes foi encontrada."
516
+ tell default account
517
+ tell default folder
518
+ set newNote to make new note with properties {name:noteTitle, body:noteBody}
519
+ show newNote
520
+ end tell
521
+ end tell
522
+ end tell
523
+ `;
524
+ await this.runCommand("osascript", ["-e", script]);
525
+ return noteTitle;
526
+ }
406
527
  async focusApp(app) {
407
528
  await this.runCommand("osascript", ["-e", `tell application "${escapeAppleScript(app)}" to activate`]);
408
529
  }
@@ -512,6 +633,9 @@ export class NativeMacOSJobExecutor {
512
633
  if (action.type === "press_shortcut") {
513
634
  return `Atalho ${action.shortcut} executado no macOS`;
514
635
  }
636
+ if (action.type === "create_note") {
637
+ return `Nota criada no Notes: ${action.title || deriveNoteTitle(action.text)}`;
638
+ }
515
639
  if (action.type === "type_text") {
516
640
  return "Texto digitado no aplicativo ativo";
517
641
  }
@@ -527,7 +651,8 @@ export class NativeMacOSJobExecutor {
527
651
  if (action.type === "run_shell") {
528
652
  return `Comando ${action.command} executado no macOS`;
529
653
  }
530
- return `${action.url} foi aberto${action.app ? ` em ${action.app}` : ""}`;
654
+ const target = humanizeUrl(action.url);
655
+ return `${target} foi aberto${action.app ? ` em ${action.app}` : ""}`;
531
656
  }
532
657
  async runCommand(command, args) {
533
658
  await this.runCommandCapture(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.4.0";
2
+ export const BRIDGE_VERSION = "0.4.2";
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.4.0",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",