@leg3ndy/otto-bridge 0.4.1 → 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.
@@ -109,6 +109,47 @@ function clipText(value, maxLength) {
109
109
  }
110
110
  return `${value.slice(0, maxLength)}...`;
111
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
+ }
112
153
  function isSafeShellCommand(command) {
113
154
  const trimmed = command.trim();
114
155
  if (!trimmed) {
@@ -235,6 +276,18 @@ function parseStructuredActions(job) {
235
276
  }
236
277
  continue;
237
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
+ }
238
291
  if (type === "type_text" || type === "write_text" || type === "keystroke") {
239
292
  const text = asString(action.text) || asString(action.content);
240
293
  if (text) {
@@ -278,6 +331,32 @@ function parseStructuredActions(job) {
278
331
  }
279
332
  return actions;
280
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
+ }
281
360
  function deriveActionsFromText(job) {
282
361
  const task = extractTaskText(job);
283
362
  const detectedApp = detectKnownApp(task);
@@ -300,7 +379,7 @@ function deriveActionsFromText(job) {
300
379
  function extractActions(job) {
301
380
  const structured = parseStructuredActions(job);
302
381
  if (structured.length > 0) {
303
- return structured;
382
+ return collapseLegacyNotePlan(structured);
304
383
  }
305
384
  return deriveActionsFromText(job);
306
385
  }
@@ -347,6 +426,12 @@ export class NativeMacOSJobExecutor {
347
426
  await this.pressShortcut(action.shortcut);
348
427
  continue;
349
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
+ }
350
435
  if (action.type === "type_text") {
351
436
  await reporter.progress(progressPercent, "Digitando texto no app ativo");
352
437
  await this.typeText(action.text);
@@ -418,6 +503,27 @@ export class NativeMacOSJobExecutor {
418
503
  }
419
504
  await this.runCommand("open", [url]);
420
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
+ }
421
527
  async focusApp(app) {
422
528
  await this.runCommand("osascript", ["-e", `tell application "${escapeAppleScript(app)}" to activate`]);
423
529
  }
@@ -527,6 +633,9 @@ export class NativeMacOSJobExecutor {
527
633
  if (action.type === "press_shortcut") {
528
634
  return `Atalho ${action.shortcut} executado no macOS`;
529
635
  }
636
+ if (action.type === "create_note") {
637
+ return `Nota criada no Notes: ${action.title || deriveNoteTitle(action.text)}`;
638
+ }
530
639
  if (action.type === "type_text") {
531
640
  return "Texto digitado no aplicativo ativo";
532
641
  }
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.4.1";
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.1",
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.",