@leg3ndy/otto-bridge 0.5.3 → 0.5.5
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/dist/config.js +25 -0
- package/dist/executors/native_macos.js +49 -5
- package/dist/main.js +65 -2
- package/dist/runtime.js +75 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -33,6 +33,26 @@ function sanitizePollIntervalMs(value, fallback = DEFAULT_CLAWD_CURSOR_POLL_INTE
|
|
|
33
33
|
}
|
|
34
34
|
return Math.max(250, Math.floor(parsed));
|
|
35
35
|
}
|
|
36
|
+
export function normalizeInstalledExtensions(values) {
|
|
37
|
+
if (!Array.isArray(values)) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const normalized = [];
|
|
42
|
+
for (const item of values) {
|
|
43
|
+
const slug = String(item || "")
|
|
44
|
+
.trim()
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
47
|
+
.replace(/^-+|-+$/g, "");
|
|
48
|
+
if (!slug || seen.has(slug)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
seen.add(slug);
|
|
52
|
+
normalized.push(slug);
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
36
56
|
function migrateLegacyExecutor(current) {
|
|
37
57
|
if (platform() === "darwin"
|
|
38
58
|
&& current?.type === "clawd-cursor"
|
|
@@ -67,7 +87,11 @@ export async function loadBridgeConfig() {
|
|
|
67
87
|
...parsed,
|
|
68
88
|
apiBaseUrl: sanitizeApiBaseUrl(parsed.apiBaseUrl),
|
|
69
89
|
wsUrl: buildWebSocketUrl(parsed.apiBaseUrl),
|
|
90
|
+
// Older pairings may have persisted an outdated bridgeVersion in config.json.
|
|
91
|
+
// The runtime must always report the currently installed package version.
|
|
92
|
+
bridgeVersion: BRIDGE_VERSION,
|
|
70
93
|
executor: resolveExecutorConfig(undefined, migrateLegacyExecutor(parsed.executor)),
|
|
94
|
+
installedExtensions: normalizeInstalledExtensions(parsed.installedExtensions),
|
|
71
95
|
};
|
|
72
96
|
}
|
|
73
97
|
catch {
|
|
@@ -144,6 +168,7 @@ export function buildBridgeConfig(params) {
|
|
|
144
168
|
approvalMode: params.approvalMode || "preview",
|
|
145
169
|
capabilities: Array.isArray(params.capabilities) ? [...params.capabilities] : [],
|
|
146
170
|
metadata: params.metadata || {},
|
|
171
|
+
installedExtensions: [],
|
|
147
172
|
pairedAt: new Date().toISOString(),
|
|
148
173
|
executor: resolveExecutorConfig(undefined, params.executor),
|
|
149
174
|
};
|
|
@@ -288,6 +288,12 @@ function clipText(value, maxLength) {
|
|
|
288
288
|
}
|
|
289
289
|
return `${value.slice(0, maxLength)}...`;
|
|
290
290
|
}
|
|
291
|
+
function clipTextPreview(value, maxLength) {
|
|
292
|
+
if (value.length <= maxLength) {
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
return `${value.slice(0, maxLength)}\n\n[conteudo truncado: mostrando ${maxLength} de ${value.length} caracteres. Peca um trecho mais especifico se quiser continuar.]`;
|
|
296
|
+
}
|
|
291
297
|
const TEXTUTIL_READABLE_EXTENSIONS = new Set([
|
|
292
298
|
".doc",
|
|
293
299
|
".docx",
|
|
@@ -1020,6 +1026,8 @@ end tell
|
|
|
1020
1026
|
title: page.title,
|
|
1021
1027
|
url: page.url,
|
|
1022
1028
|
text: page.text,
|
|
1029
|
+
playerTitle: page.playerTitle || "",
|
|
1030
|
+
playerState: page.playerState || "",
|
|
1023
1031
|
};
|
|
1024
1032
|
}
|
|
1025
1033
|
resolveExpectedBrowserHref(rawHref, baseUrl) {
|
|
@@ -1058,6 +1066,22 @@ end tell
|
|
|
1058
1066
|
return true;
|
|
1059
1067
|
}
|
|
1060
1068
|
}
|
|
1069
|
+
const beforePlayerTitle = normalizeText(before?.playerTitle || "");
|
|
1070
|
+
const afterPlayerTitle = normalizeText(after.playerTitle || "");
|
|
1071
|
+
const beforePlayerState = normalizeText(before?.playerState || "");
|
|
1072
|
+
const afterPlayerState = normalizeText(after.playerState || "");
|
|
1073
|
+
const playerLooksActive = afterPlayerState.includes("pause") || afterPlayerState.includes("pausar");
|
|
1074
|
+
if (afterUrl.includes("music.youtube.com")) {
|
|
1075
|
+
if (beforePlayerState && afterPlayerState && beforePlayerState !== afterPlayerState && playerLooksActive) {
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
if (beforePlayerTitle && afterPlayerTitle && beforePlayerTitle !== afterPlayerTitle) {
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
if (!beforePlayerTitle && afterPlayerTitle && playerLooksActive) {
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1061
1085
|
const beforeTitle = normalizeText(before?.title || "");
|
|
1062
1086
|
const afterTitle = normalizeText(after.title || "");
|
|
1063
1087
|
if (beforeTitle && afterTitle && beforeTitle !== afterTitle) {
|
|
@@ -1259,6 +1283,7 @@ const normalize = (value) => String(value || "")
|
|
|
1259
1283
|
.replace(/[\\u0300-\\u036f]/g, "")
|
|
1260
1284
|
.toLowerCase();
|
|
1261
1285
|
const normalizedDescription = normalize(rawDescription);
|
|
1286
|
+
const isYouTubeMusic = location.hostname.includes("music.youtube.com");
|
|
1262
1287
|
const wantsFirst = /\\b(primeir[ao]?|first)\\b/.test(normalizedDescription);
|
|
1263
1288
|
const wantsVideo = /\\b(video|videos|musica|faixa|youtube|resultado|watch)\\b/.test(normalizedDescription) || location.hostname.includes("youtube");
|
|
1264
1289
|
const stopWords = new Set([
|
|
@@ -1275,7 +1300,19 @@ const tokens = Array.from(new Set(
|
|
|
1275
1300
|
.filter((token) => token.length >= 3 && !stopWords.has(token))
|
|
1276
1301
|
));
|
|
1277
1302
|
|
|
1278
|
-
const candidateSelectors =
|
|
1303
|
+
const candidateSelectors = isYouTubeMusic
|
|
1304
|
+
? [
|
|
1305
|
+
"ytmusic-responsive-list-item-renderer a[href*='watch?v=']",
|
|
1306
|
+
"ytmusic-responsive-list-item-renderer button[aria-label]",
|
|
1307
|
+
"ytmusic-responsive-list-item-renderer tp-yt-paper-icon-button",
|
|
1308
|
+
"ytmusic-responsive-list-item-renderer ytmusic-item-thumbnail-overlay-renderer button",
|
|
1309
|
+
"ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer a[href*='watch?v=']",
|
|
1310
|
+
"a[href*='watch?v=']",
|
|
1311
|
+
"button",
|
|
1312
|
+
"[role='button']",
|
|
1313
|
+
"[role='link']"
|
|
1314
|
+
]
|
|
1315
|
+
: location.hostname.includes("youtube")
|
|
1279
1316
|
? [
|
|
1280
1317
|
"ytd-video-renderer a#video-title",
|
|
1281
1318
|
"ytd-video-renderer ytd-thumbnail a",
|
|
@@ -1339,8 +1376,11 @@ function scoreCandidate(element, rank) {
|
|
|
1339
1376
|
|
|
1340
1377
|
if (wantsFirst) score += Math.max(0, 40 - rank);
|
|
1341
1378
|
if (wantsVideo && normalizedHref.includes("/watch")) score += 30;
|
|
1379
|
+
if (isYouTubeMusic && normalizedHref.includes("watch?v=")) score += 36;
|
|
1380
|
+
if (isYouTubeMusic && element.closest("ytmusic-responsive-list-item-renderer, ytmusic-player-bar")) score += 24;
|
|
1342
1381
|
if (location.hostname.includes("youtube") && element.closest("ytd-video-renderer, ytd-rich-item-renderer, ytd-rich-grid-media")) score += 20;
|
|
1343
1382
|
if (element.id === "video-title") score += 12;
|
|
1383
|
+
if (isYouTubeMusic && /\\b(play|pause|reproduzir|tocar)\\b/.test(normalizedText)) score += 12;
|
|
1344
1384
|
if (!normalizedText && normalizedHref.includes("/watch")) score += 8;
|
|
1345
1385
|
|
|
1346
1386
|
for (const phrase of quotedPhrases) {
|
|
@@ -1445,7 +1485,7 @@ tell application "Safari"
|
|
|
1445
1485
|
activate
|
|
1446
1486
|
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
1447
1487
|
delay 1
|
|
1448
|
-
set pageJson to do JavaScript "(function(){const title=document.title||''; const url=location.href||''; const text=((document.body&&document.body.innerText)||'').trim().slice(0, 12000); return JSON.stringify({title:title,url:url,text:text});})();" in current tab of front window
|
|
1488
|
+
set pageJson to do JavaScript "(function(){const title=document.title||''; const url=location.href||''; const text=((document.body&&document.body.innerText)||'').trim().slice(0, 12000); const playerButton=document.querySelector('ytmusic-player-bar #play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button#play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button.play-pause-button'); const playerTitle=(Array.from(document.querySelectorAll('ytmusic-player-bar .title, ytmusic-player-bar .content-info-wrapper .title, ytmusic-player-bar [slot=\"title\"]')).map((node)=>((node&&node.textContent)||'').trim()).find(Boolean))||''; const playerState=(playerButton&&((playerButton.getAttribute('title')||playerButton.getAttribute('aria-label')||playerButton.textContent)||'').trim())||''; return JSON.stringify({title:title,url:url,text:text,playerTitle:playerTitle,playerState:playerState});})();" in current tab of front window
|
|
1449
1489
|
end tell
|
|
1450
1490
|
return pageJson
|
|
1451
1491
|
`;
|
|
@@ -1456,6 +1496,8 @@ return pageJson
|
|
|
1456
1496
|
title: asString(parsed.title) || "",
|
|
1457
1497
|
url: asString(parsed.url) || "",
|
|
1458
1498
|
text: asString(parsed.text) || "",
|
|
1499
|
+
playerTitle: asString(parsed.playerTitle) || "",
|
|
1500
|
+
playerState: asString(parsed.playerState) || "",
|
|
1459
1501
|
};
|
|
1460
1502
|
}
|
|
1461
1503
|
catch (error) {
|
|
@@ -1478,6 +1520,8 @@ return pageTitle & linefeed & pageUrl
|
|
|
1478
1520
|
title: String(title || "").trim(),
|
|
1479
1521
|
url: String(url || "").trim(),
|
|
1480
1522
|
text: "",
|
|
1523
|
+
playerTitle: "",
|
|
1524
|
+
playerState: "",
|
|
1481
1525
|
};
|
|
1482
1526
|
}
|
|
1483
1527
|
}
|
|
@@ -1715,7 +1759,7 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
1715
1759
|
resized,
|
|
1716
1760
|
};
|
|
1717
1761
|
}
|
|
1718
|
-
async readLocalFile(filePath, maxChars =
|
|
1762
|
+
async readLocalFile(filePath, maxChars = 1800) {
|
|
1719
1763
|
const resolved = expandUserPath(filePath);
|
|
1720
1764
|
const extension = path.extname(resolved).toLowerCase();
|
|
1721
1765
|
if (TEXTUTIL_READABLE_EXTENSIONS.has(extension)) {
|
|
@@ -1726,7 +1770,7 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
1726
1770
|
resolved,
|
|
1727
1771
|
]);
|
|
1728
1772
|
const content = sanitizeTextForJsonTransport(stdout);
|
|
1729
|
-
return
|
|
1773
|
+
return clipTextPreview(content || "(arquivo sem texto legivel)", maxChars);
|
|
1730
1774
|
}
|
|
1731
1775
|
const raw = await readFile(resolved);
|
|
1732
1776
|
if (isLikelyBinaryBuffer(raw)) {
|
|
@@ -1735,7 +1779,7 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
1735
1779
|
return clipText(`O arquivo ${filename} parece ser binario (${detectedType}) e nao pode ser lido como texto puro pelo Otto Bridge ainda.`, maxChars);
|
|
1736
1780
|
}
|
|
1737
1781
|
const content = sanitizeTextForJsonTransport(raw.toString("utf8"));
|
|
1738
|
-
return
|
|
1782
|
+
return clipTextPreview(content || "(arquivo vazio)", maxChars);
|
|
1739
1783
|
}
|
|
1740
1784
|
async listLocalFiles(directoryPath, limit = 40) {
|
|
1741
1785
|
const resolved = expandUserPath(directoryPath);
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { clearBridgeConfig, getBridgeConfigPath, loadBridgeConfig, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
|
|
4
|
+
import { clearBridgeConfig, getBridgeConfigPath, loadBridgeConfig, normalizeInstalledExtensions, resolveApiBaseUrl, resolveExecutorConfig, saveBridgeConfig, } from "./config.js";
|
|
5
5
|
import { pairDevice } from "./pairing.js";
|
|
6
6
|
import { BridgeRuntime } from "./runtime.js";
|
|
7
7
|
import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
|
|
@@ -56,6 +56,9 @@ function printUsage() {
|
|
|
56
56
|
otto-bridge pair --api http://localhost:8000 --code ABC123 [--name "Meu PC"] [--executor native-macos|mock|clawd-cursor]
|
|
57
57
|
otto-bridge run [--executor native-macos|mock|clawd-cursor] [--clawd-url http://127.0.0.1:3847]
|
|
58
58
|
otto-bridge status
|
|
59
|
+
otto-bridge extensions --list
|
|
60
|
+
otto-bridge extensions --install github
|
|
61
|
+
otto-bridge extensions --uninstall github
|
|
59
62
|
otto-bridge version
|
|
60
63
|
otto-bridge update [--tag latest|next] [--dry-run]
|
|
61
64
|
otto-bridge unpair
|
|
@@ -63,6 +66,8 @@ function printUsage() {
|
|
|
63
66
|
Examples:
|
|
64
67
|
otto-bridge pair --api https://api.leg3ndy.com.br --code ABC123
|
|
65
68
|
otto-bridge run
|
|
69
|
+
otto-bridge extensions --install github
|
|
70
|
+
otto-bridge extensions --list
|
|
66
71
|
otto-bridge version
|
|
67
72
|
otto-bridge update
|
|
68
73
|
otto-bridge update --dry-run
|
|
@@ -108,11 +113,15 @@ async function runPairCommand(args) {
|
|
|
108
113
|
console.log(`[otto-bridge] config=${getBridgeConfigPath()}`);
|
|
109
114
|
console.log("[otto-bridge] next step: run `otto-bridge run` to keep this device online");
|
|
110
115
|
}
|
|
111
|
-
async function
|
|
116
|
+
async function loadRequiredBridgeConfig() {
|
|
112
117
|
const config = await loadBridgeConfig();
|
|
113
118
|
if (!config) {
|
|
114
119
|
throw new Error("No local pairing found. Run `otto-bridge pair --code <CODE>` first.");
|
|
115
120
|
}
|
|
121
|
+
return config;
|
|
122
|
+
}
|
|
123
|
+
async function runRuntimeCommand(args) {
|
|
124
|
+
const config = await loadRequiredBridgeConfig();
|
|
116
125
|
const runtimeConfig = {
|
|
117
126
|
...config,
|
|
118
127
|
executor: resolveExecutorOverrides(args, config.executor),
|
|
@@ -136,10 +145,61 @@ async function runStatusCommand() {
|
|
|
136
145
|
ws_url: config.wsUrl,
|
|
137
146
|
approval_mode: config.approvalMode,
|
|
138
147
|
capabilities: config.capabilities,
|
|
148
|
+
installed_extensions: config.installedExtensions,
|
|
139
149
|
paired_at: config.pairedAt,
|
|
140
150
|
executor: config.executor,
|
|
141
151
|
}, null, 2));
|
|
142
152
|
}
|
|
153
|
+
async function runExtensionsCommand(args) {
|
|
154
|
+
const config = await loadRequiredBridgeConfig();
|
|
155
|
+
const installValue = option(args, "install");
|
|
156
|
+
const uninstallValue = option(args, "uninstall");
|
|
157
|
+
if (installValue && uninstallValue) {
|
|
158
|
+
throw new Error("Use apenas uma acao por vez: --install ou --uninstall.");
|
|
159
|
+
}
|
|
160
|
+
if (installValue) {
|
|
161
|
+
const nextExtensions = normalizeInstalledExtensions([
|
|
162
|
+
...config.installedExtensions,
|
|
163
|
+
...installValue.split(","),
|
|
164
|
+
]);
|
|
165
|
+
const added = nextExtensions.filter((item) => !config.installedExtensions.includes(item));
|
|
166
|
+
if (!added.length) {
|
|
167
|
+
console.log("[otto-bridge] nenhuma extensao nova para instalar");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await saveBridgeConfig({
|
|
171
|
+
...config,
|
|
172
|
+
installedExtensions: nextExtensions,
|
|
173
|
+
});
|
|
174
|
+
console.log(`[otto-bridge] extensoes instaladas: ${added.join(", ")}`);
|
|
175
|
+
console.log("[otto-bridge] rode `otto-bridge run` novamente se quiser sincronizar agora com a web");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (uninstallValue) {
|
|
179
|
+
const removeSet = new Set(normalizeInstalledExtensions(uninstallValue.split(",")));
|
|
180
|
+
const nextExtensions = config.installedExtensions.filter((item) => !removeSet.has(item));
|
|
181
|
+
const removed = config.installedExtensions.filter((item) => removeSet.has(item));
|
|
182
|
+
if (!removed.length) {
|
|
183
|
+
console.log("[otto-bridge] nenhuma extensao correspondente estava instalada");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await saveBridgeConfig({
|
|
187
|
+
...config,
|
|
188
|
+
installedExtensions: nextExtensions,
|
|
189
|
+
});
|
|
190
|
+
console.log(`[otto-bridge] extensoes removidas: ${removed.join(", ")}`);
|
|
191
|
+
console.log("[otto-bridge] rode `otto-bridge run` novamente se quiser sincronizar agora com a web");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!config.installedExtensions.length) {
|
|
195
|
+
console.log("[otto-bridge] nenhuma extensao instalada");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.log("[otto-bridge] extensoes instaladas:");
|
|
199
|
+
for (const extension of config.installedExtensions) {
|
|
200
|
+
console.log(`- ${extension}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
143
203
|
async function runUnpairCommand() {
|
|
144
204
|
await clearBridgeConfig();
|
|
145
205
|
console.log("[otto-bridge] local pairing cleared");
|
|
@@ -173,6 +233,9 @@ async function main() {
|
|
|
173
233
|
case "status":
|
|
174
234
|
await runStatusCommand();
|
|
175
235
|
return;
|
|
236
|
+
case "extensions":
|
|
237
|
+
await runExtensionsCommand(args);
|
|
238
|
+
return;
|
|
176
239
|
case "version":
|
|
177
240
|
printVersion();
|
|
178
241
|
return;
|
package/dist/runtime.js
CHANGED
|
@@ -6,6 +6,52 @@ import { JobCancelledError } from "./executors/shared.js";
|
|
|
6
6
|
function delay(ms) {
|
|
7
7
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
8
|
}
|
|
9
|
+
function parseSemverTuple(value) {
|
|
10
|
+
const text = String(value || "").trim().replace(/^[vV]/, "");
|
|
11
|
+
if (!text) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const parts = text.split(".");
|
|
15
|
+
const parsed = [];
|
|
16
|
+
for (const part of parts) {
|
|
17
|
+
const match = part.match(/^(\d+)/);
|
|
18
|
+
if (!match) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
parsed.push(Number(match[1]));
|
|
22
|
+
}
|
|
23
|
+
return parsed.length > 0 ? parsed : null;
|
|
24
|
+
}
|
|
25
|
+
function compareSemver(left, right) {
|
|
26
|
+
const a = parseSemverTuple(left);
|
|
27
|
+
const b = parseSemverTuple(right);
|
|
28
|
+
if (!a && !b)
|
|
29
|
+
return 0;
|
|
30
|
+
if (!a)
|
|
31
|
+
return -1;
|
|
32
|
+
if (!b)
|
|
33
|
+
return 1;
|
|
34
|
+
const maxLength = Math.max(a.length, b.length);
|
|
35
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
36
|
+
const leftPart = a[index] ?? 0;
|
|
37
|
+
const rightPart = b[index] ?? 0;
|
|
38
|
+
if (leftPart < rightPart)
|
|
39
|
+
return -1;
|
|
40
|
+
if (leftPart > rightPart)
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
function bridgeReleaseFromMessage(message) {
|
|
46
|
+
const nested = message.bridge_release;
|
|
47
|
+
if (nested && typeof nested === "object") {
|
|
48
|
+
return nested;
|
|
49
|
+
}
|
|
50
|
+
if (message.latest_version || message.min_supported_version || message.update_command) {
|
|
51
|
+
return message;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
9
55
|
async function parseSocketMessage(data) {
|
|
10
56
|
if (typeof data === "string") {
|
|
11
57
|
return JSON.parse(data);
|
|
@@ -25,6 +71,7 @@ export class BridgeRuntime {
|
|
|
25
71
|
config;
|
|
26
72
|
reconnectDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
27
73
|
executor;
|
|
74
|
+
lastBridgeReleaseNoticeKey = null;
|
|
28
75
|
pendingConfirmations = new Map();
|
|
29
76
|
activeCancels = new Map();
|
|
30
77
|
constructor(config, executor) {
|
|
@@ -71,7 +118,10 @@ export class BridgeRuntime {
|
|
|
71
118
|
device_name: this.config.deviceName,
|
|
72
119
|
bridge_version: this.config.bridgeVersion,
|
|
73
120
|
capabilities: this.config.capabilities,
|
|
74
|
-
metadata:
|
|
121
|
+
metadata: {
|
|
122
|
+
...(this.config.metadata || {}),
|
|
123
|
+
installed_extensions: this.config.installedExtensions,
|
|
124
|
+
},
|
|
75
125
|
}));
|
|
76
126
|
heartbeatTimer = setInterval(() => {
|
|
77
127
|
if (socket.readyState === WebSocket.OPEN) {
|
|
@@ -119,6 +169,7 @@ export class BridgeRuntime {
|
|
|
119
169
|
console.log(`[otto-bridge] server hello device=${String(message.device_id || "")}`);
|
|
120
170
|
return;
|
|
121
171
|
case "device.hello_ack":
|
|
172
|
+
this.maybeLogBridgeReleaseNotice(message);
|
|
122
173
|
case "device.heartbeat_ack":
|
|
123
174
|
return;
|
|
124
175
|
case "device.job.start":
|
|
@@ -145,6 +196,29 @@ export class BridgeRuntime {
|
|
|
145
196
|
console.log(`[otto-bridge] event=${type || "unknown"} payload=${JSON.stringify(message)}`);
|
|
146
197
|
}
|
|
147
198
|
}
|
|
199
|
+
maybeLogBridgeReleaseNotice(message) {
|
|
200
|
+
const release = bridgeReleaseFromMessage(message);
|
|
201
|
+
if (!release) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const latestVersion = String(release.latest_version || "").trim();
|
|
205
|
+
const minSupportedVersion = String(release.min_supported_version || "").trim();
|
|
206
|
+
const updateCommand = String(release.update_command || "otto-bridge update").trim() || "otto-bridge update";
|
|
207
|
+
const updateRequired = release.update_required === true || (minSupportedVersion ? compareSemver(this.config.bridgeVersion, minSupportedVersion) < 0 : false);
|
|
208
|
+
const updateAvailable = release.update_available === true || (latestVersion ? compareSemver(this.config.bridgeVersion, latestVersion) < 0 : false);
|
|
209
|
+
const noticeKey = [latestVersion, minSupportedVersion, updateRequired ? "req" : "ok", updateAvailable ? "avail" : "cur"].join("|");
|
|
210
|
+
if (!noticeKey.trim() || this.lastBridgeReleaseNoticeKey === noticeKey) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.lastBridgeReleaseNoticeKey = noticeKey;
|
|
214
|
+
if (updateRequired) {
|
|
215
|
+
console.warn(`[otto-bridge] update required current=${this.config.bridgeVersion} min_supported=${minSupportedVersion || "unknown"} latest=${latestVersion || "unknown"} command="${updateCommand}"`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (updateAvailable) {
|
|
219
|
+
console.log(`[otto-bridge] update available current=${this.config.bridgeVersion} latest=${latestVersion || "unknown"} command="${updateCommand}"`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
148
222
|
resolveConfirmation(message) {
|
|
149
223
|
const jobId = String(message.job_id || "");
|
|
150
224
|
const action = String(message.action || "").trim().toLowerCase();
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.5.
|
|
2
|
+
export const BRIDGE_VERSION = "0.5.5";
|
|
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;
|