@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 +1 -1
- package/dist/executors/native_macos.js +136 -11
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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, "<")
|
|
116
|
+
.replace(/>/g, ">");
|
|
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
|
-
|
|
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.
|
|
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;
|