@leg3ndy/otto-bridge 1.0.0 → 1.0.1
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 +24 -22
- package/dist/chat_cli_client.js +91 -0
- package/dist/cli_terminal.js +252 -74
- package/dist/local_automations.js +15 -10
- package/dist/main.js +2 -3
- package/dist/runtime.js +47 -21
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Para o estado atual da arquitetura, capacidades entregues, limitacoes e roadmap
|
|
|
15
15
|
|
|
16
16
|
Para o corte de arquitetura do `0.9.0`, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_0_RELEASE.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_0_RELEASE.md).
|
|
17
17
|
|
|
18
|
-
Para a release atual `1.0.
|
|
18
|
+
Para a release atual `1.0.1`, com runtime agentico formal, hub terminal e console alinhado ao Otto da web, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_1_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_1_PATCH.md).
|
|
19
19
|
|
|
20
20
|
## Distribuicao
|
|
21
21
|
|
|
@@ -38,14 +38,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
npm pack
|
|
41
|
-
npm install -g ./leg3ndy-otto-bridge-1.0.
|
|
41
|
+
npm install -g ./leg3ndy-otto-bridge-1.0.1.tgz
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Na linha `1.0.
|
|
44
|
+
Na linha `1.0.1`, `playwright` segue como dependencia obrigatoria no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
|
|
45
45
|
|
|
46
|
-
No macOS, a linha `1.0.
|
|
46
|
+
No macOS, a linha `1.0.1` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
|
|
47
47
|
|
|
48
|
-
No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `1.0.0`
|
|
48
|
+
No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `1.0.0` oficializou isso como runtime agentico; o `1.0.1` consolida o hub terminal como fluxo principal e alinha o console ao comportamento normal do Otto antes de fazer handoff local.
|
|
49
49
|
|
|
50
50
|
## Publicacao
|
|
51
51
|
|
|
@@ -76,14 +76,14 @@ otto-bridge help
|
|
|
76
76
|
otto-bridge --help
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
### Abrir o hub interativo
|
|
79
|
+
### Abrir o hub interativo e ligar o runtime
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
82
|
otto-bridge
|
|
83
83
|
otto-bridge home
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
Em TTY, o comando sem argumentos agora abre o hub interativo com banner, setup, status, extensoes e o `Otto Console`.
|
|
86
|
+
Em TTY, o comando sem argumentos agora abre o hub interativo com banner, setup, status, extensoes e o `Otto Console`. Se ja existir pairing salvo, o próprio `otto-bridge` sobe o runtime local automaticamente e mostra o estado da conexão no hub.
|
|
87
87
|
|
|
88
88
|
### Setup interativo
|
|
89
89
|
|
|
@@ -110,24 +110,26 @@ Opcoes suportadas:
|
|
|
110
110
|
|
|
111
111
|
No macOS, o caminho recomendado agora e o executor nativo do Otto Bridge. Se nenhum `--executor` for informado, o `pair` usa `native-macos` por padrao no Mac.
|
|
112
112
|
|
|
113
|
-
###
|
|
113
|
+
### Runtime principal do bridge
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
|
-
otto-bridge
|
|
116
|
+
otto-bridge
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
Esse agora e o fluxo principal do produto: o hub abre, conecta o runtime local e deixa o Otto pronto para handoff e approvals sem depender de um `run` separado.
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
Se precisar do executor nativo no macOS sem reparar, atualize o pairing/config e reabra o hub:
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
|
-
otto-bridge
|
|
124
|
+
otto-bridge setup
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
`otto-bridge run` continua existindo apenas como alias legado/headless para compatibilidade operacional.
|
|
128
|
+
|
|
129
|
+
O adapter `clawd-cursor` continua disponivel como override opcional no pairing legado:
|
|
128
130
|
|
|
129
131
|
```bash
|
|
130
|
-
otto-bridge
|
|
132
|
+
otto-bridge pair --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
131
133
|
```
|
|
132
134
|
|
|
133
135
|
### Falar com o Otto no terminal
|
|
@@ -136,11 +138,11 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
|
136
138
|
otto-bridge console
|
|
137
139
|
```
|
|
138
140
|
|
|
139
|
-
O console
|
|
141
|
+
O console usa a mesma sessão local já ligada pelo `otto-bridge`, envia prompts naturais ao backend usando `device_token`, respeita quota/plano do usuário e só vira handoff local quando o pedido realmente tiver cara de ação no computador. Quando houver `device_job`, ele acompanha polling e resolve `confirm_required` no terminal.
|
|
140
142
|
|
|
141
143
|
### WhatsApp Web em background
|
|
142
144
|
|
|
143
|
-
Fluxo recomendado na linha `1.0.
|
|
145
|
+
Fluxo recomendado na linha `1.0.1`:
|
|
144
146
|
|
|
145
147
|
```bash
|
|
146
148
|
otto-bridge extensions --install whatsappweb
|
|
@@ -150,13 +152,13 @@ otto-bridge extensions --status whatsappweb
|
|
|
150
152
|
|
|
151
153
|
O setup agora abre o login do WhatsApp Web no helper/background browser do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
|
|
152
154
|
|
|
153
|
-
Contrato da linha `1.0.
|
|
155
|
+
Contrato da linha `1.0.1`:
|
|
154
156
|
|
|
155
157
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
156
|
-
- `otto-bridge
|
|
157
|
-
- ao
|
|
158
|
+
- `otto-bridge`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime do hub estiver ativo, sem depender de uma aba aberta no Safari
|
|
159
|
+
- ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
158
160
|
|
|
159
|
-
## Handoff rapido da linha 1.0.
|
|
161
|
+
## Handoff rapido da linha 1.0.1
|
|
160
162
|
|
|
161
163
|
Ja fechado no codigo:
|
|
162
164
|
|
|
@@ -190,8 +192,8 @@ Ja fechado no codigo:
|
|
|
190
192
|
- o `instruction_updater` agora e um descriptor explicito do runtime, com `source_entrypoints`, `target_paths` e `recommended_validation_stage_ids` quando o plano toca `AGENTS.md`, `README.md` ou `docs/`
|
|
191
193
|
- `workspace.tests` agora consegue resolver `profile=auto` via `validation_ladder`, executando stages como `typecheck`, `build`, `node_test`, `lint` e `pytest` por stack, com snapshot agregado por etapa
|
|
192
194
|
- o executor `native-macos` agora publica `runtime_hook_trace` somando hooks de lifecycle e tool-use (`session_start`, `pre_tool_use`, `post_tool_use`, `validation_ladder_started/completed`, `session_end`), preparando enforcement, replay fino e metricas futuras
|
|
193
|
-
- o bridge agora possui um CLI interativo proprio com setup inicial, hub terminal e `Otto Console` para
|
|
194
|
-
- o backend agora expoe
|
|
195
|
+
- o bridge agora possui um CLI interativo proprio com setup inicial, hub terminal e `Otto Console` para conversar com o Otto pelo proprio terminal
|
|
196
|
+
- o backend agora expoe caminhos `device-auth` para o console do bridge usar chat com quota/plano (`/v1/devices/cli/chat/completions`) e acompanhar approval/job usando apenas `device_token`
|
|
195
197
|
- o caminho principal do bridge usa `summary`/`narration_context` no lugar de resposta automatica pronta
|
|
196
198
|
- `read_file` agora entrega conteudo completo segmentado em `content_chunks` para o Otto
|
|
197
199
|
- `list_files` sem `limit` agora lista o diretorio inteiro, sem fallback silencioso para 40 itens
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function normalizeBaseUrl(apiBaseUrl) {
|
|
2
|
+
return String(apiBaseUrl || "").trim().replace(/\/+$/, "");
|
|
3
|
+
}
|
|
4
|
+
function buildDeviceAuthHeaders(deviceToken, headers) {
|
|
5
|
+
const next = new Headers(headers || {});
|
|
6
|
+
if (deviceToken) {
|
|
7
|
+
next.set("Authorization", `Bearer ${deviceToken}`);
|
|
8
|
+
}
|
|
9
|
+
return next;
|
|
10
|
+
}
|
|
11
|
+
function parseApiError(payload, fallbackStatus) {
|
|
12
|
+
if (payload && typeof payload === "object") {
|
|
13
|
+
const detail = "detail" in payload ? payload.detail : null;
|
|
14
|
+
if (typeof detail === "string" && detail.trim()) {
|
|
15
|
+
return new Error(detail.trim());
|
|
16
|
+
}
|
|
17
|
+
if (detail && typeof detail === "object") {
|
|
18
|
+
const message = "message" in detail ? detail.message : null;
|
|
19
|
+
if (typeof message === "string" && message.trim()) {
|
|
20
|
+
return new Error(message.trim());
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const error = "error" in payload ? payload.error : null;
|
|
24
|
+
if (typeof error === "string" && error.trim()) {
|
|
25
|
+
return new Error(error.trim());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return new Error(`HTTP ${fallbackStatus}`);
|
|
29
|
+
}
|
|
30
|
+
export async function streamDeviceCliChat(config, request, onEvent) {
|
|
31
|
+
const url = `${normalizeBaseUrl(config.apiBaseUrl)}/v1/devices/cli/chat/completions`;
|
|
32
|
+
let response;
|
|
33
|
+
try {
|
|
34
|
+
response = await fetch(url, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: buildDeviceAuthHeaders(config.deviceToken, {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
}),
|
|
39
|
+
body: JSON.stringify(request),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
44
|
+
throw new Error(`Request failed for ${url}: ${detail}`);
|
|
45
|
+
}
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const payload = await response.json().catch(() => null);
|
|
48
|
+
throw parseApiError(payload, response.status);
|
|
49
|
+
}
|
|
50
|
+
if (!response.body) {
|
|
51
|
+
throw new Error("Empty stream response");
|
|
52
|
+
}
|
|
53
|
+
const reader = response.body.getReader();
|
|
54
|
+
const decoder = new TextDecoder();
|
|
55
|
+
let buffer = "";
|
|
56
|
+
const flushBlock = async (rawBlock) => {
|
|
57
|
+
const lines = rawBlock
|
|
58
|
+
.split("\n")
|
|
59
|
+
.map((line) => line.trimEnd())
|
|
60
|
+
.filter((line) => line.startsWith("data:"));
|
|
61
|
+
if (!lines.length) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const payloadText = lines
|
|
65
|
+
.map((line) => line.slice(5).trimStart())
|
|
66
|
+
.join("\n")
|
|
67
|
+
.trim();
|
|
68
|
+
if (!payloadText) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const payload = JSON.parse(payloadText);
|
|
72
|
+
await onEvent(payload);
|
|
73
|
+
};
|
|
74
|
+
for (;;) {
|
|
75
|
+
const { done, value } = await reader.read();
|
|
76
|
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
77
|
+
let separatorIndex = buffer.indexOf("\n\n");
|
|
78
|
+
while (separatorIndex >= 0) {
|
|
79
|
+
const block = buffer.slice(0, separatorIndex);
|
|
80
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
81
|
+
await flushBlock(block);
|
|
82
|
+
separatorIndex = buffer.indexOf("\n\n");
|
|
83
|
+
}
|
|
84
|
+
if (done) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (buffer.trim()) {
|
|
89
|
+
await flushBlock(buffer);
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/cli_terminal.js
CHANGED
|
@@ -2,10 +2,11 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
4
|
import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
|
|
5
|
+
import { streamDeviceCliChat, } from "./chat_cli_client.js";
|
|
5
6
|
import { formatManagedBridgeExtensionStatus, isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
|
|
6
7
|
import { pairDevice } from "./pairing.js";
|
|
7
|
-
import { BridgeRuntime } from "./runtime.js";
|
|
8
|
-
import { cancelRuntimeCliJob, confirmRuntimeCliJob, getRuntimeCliJob,
|
|
8
|
+
import { BridgeRuntime, } from "./runtime.js";
|
|
9
|
+
import { cancelRuntimeCliJob, confirmRuntimeCliJob, getRuntimeCliJob, } from "./runtime_cli_client.js";
|
|
9
10
|
import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_API_BASE_URL, } from "./types.js";
|
|
10
11
|
const ANSI = {
|
|
11
12
|
reset: "\u001b[0m",
|
|
@@ -27,6 +28,123 @@ const OTTOAI_BANNER = [
|
|
|
27
28
|
"╚██████╔╝ ██║ ██║ ╚██████╔╝ ██║ ██║██║",
|
|
28
29
|
" ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝",
|
|
29
30
|
];
|
|
31
|
+
class CliRuntimeSession {
|
|
32
|
+
config;
|
|
33
|
+
runtime = null;
|
|
34
|
+
runtimeTask = null;
|
|
35
|
+
status = "offline";
|
|
36
|
+
detail = "Aguardando pairing.";
|
|
37
|
+
lastError = null;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
}
|
|
41
|
+
handleRuntimeEvent(event) {
|
|
42
|
+
switch (event.type) {
|
|
43
|
+
case "starting":
|
|
44
|
+
this.status = "starting";
|
|
45
|
+
this.detail = "Conectando o runtime local do Otto Bridge...";
|
|
46
|
+
return;
|
|
47
|
+
case "connected":
|
|
48
|
+
this.status = "online";
|
|
49
|
+
this.detail = "Runtime conectado ao backend e pronto para handoff local.";
|
|
50
|
+
return;
|
|
51
|
+
case "server_hello":
|
|
52
|
+
this.status = "online";
|
|
53
|
+
this.detail = "Handshake com o backend concluído.";
|
|
54
|
+
return;
|
|
55
|
+
case "reconnecting":
|
|
56
|
+
this.status = "reconnecting";
|
|
57
|
+
this.detail = `Reconectando em ${Math.max(1, Math.round(event.delayMs / 1000))}s...`;
|
|
58
|
+
return;
|
|
59
|
+
case "socket_error":
|
|
60
|
+
this.status = "error";
|
|
61
|
+
this.lastError = event.message;
|
|
62
|
+
this.detail = truncate(event.message || "Falha de conexão do runtime.", 160);
|
|
63
|
+
return;
|
|
64
|
+
case "socket_closed":
|
|
65
|
+
if (this.status !== "error") {
|
|
66
|
+
this.status = "offline";
|
|
67
|
+
this.detail = `Socket fechado (code=${event.code}).`;
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
case "update_required":
|
|
71
|
+
case "update_available":
|
|
72
|
+
this.detail = truncate(event.message || this.detail, 160);
|
|
73
|
+
return;
|
|
74
|
+
default:
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async ensureStarted() {
|
|
79
|
+
if (this.runtimeTask) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.status = "starting";
|
|
83
|
+
this.detail = "Subindo runtime local do Otto Bridge...";
|
|
84
|
+
this.runtime = new BridgeRuntime(this.config, undefined, {
|
|
85
|
+
logger: {
|
|
86
|
+
info: () => undefined,
|
|
87
|
+
warn: () => undefined,
|
|
88
|
+
error: () => undefined,
|
|
89
|
+
event: (event) => {
|
|
90
|
+
this.handleRuntimeEvent(event);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
this.runtimeTask = this.runtime.start().catch((error) => {
|
|
95
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
96
|
+
this.status = "error";
|
|
97
|
+
this.lastError = detail;
|
|
98
|
+
this.detail = truncate(detail || "Falha ao iniciar o runtime.", 160);
|
|
99
|
+
});
|
|
100
|
+
await delay(350);
|
|
101
|
+
}
|
|
102
|
+
async replaceConfig(nextConfig) {
|
|
103
|
+
await this.stop();
|
|
104
|
+
this.config = nextConfig;
|
|
105
|
+
this.status = "offline";
|
|
106
|
+
this.detail = "Reinicializando runtime com o novo pairing...";
|
|
107
|
+
this.lastError = null;
|
|
108
|
+
await this.ensureStarted();
|
|
109
|
+
}
|
|
110
|
+
async stop() {
|
|
111
|
+
const runtime = this.runtime;
|
|
112
|
+
const runtimeTask = this.runtimeTask;
|
|
113
|
+
this.runtime = null;
|
|
114
|
+
this.runtimeTask = null;
|
|
115
|
+
if (runtime) {
|
|
116
|
+
await runtime.stop().catch(() => undefined);
|
|
117
|
+
}
|
|
118
|
+
if (runtimeTask) {
|
|
119
|
+
await runtimeTask.catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
if (this.status !== "error") {
|
|
122
|
+
this.status = "offline";
|
|
123
|
+
this.detail = "Runtime local desligado.";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
getStatusLabel() {
|
|
127
|
+
if (this.status === "online") {
|
|
128
|
+
return "online";
|
|
129
|
+
}
|
|
130
|
+
if (this.status === "starting") {
|
|
131
|
+
return "starting";
|
|
132
|
+
}
|
|
133
|
+
if (this.status === "reconnecting") {
|
|
134
|
+
return "reconnecting";
|
|
135
|
+
}
|
|
136
|
+
if (this.status === "error") {
|
|
137
|
+
return "error";
|
|
138
|
+
}
|
|
139
|
+
return "offline";
|
|
140
|
+
}
|
|
141
|
+
getStatusDetail() {
|
|
142
|
+
return this.detail;
|
|
143
|
+
}
|
|
144
|
+
getLastError() {
|
|
145
|
+
return this.lastError;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
30
148
|
function style(text, color, enabled = true) {
|
|
31
149
|
if (!enabled) {
|
|
32
150
|
return text;
|
|
@@ -123,16 +241,20 @@ async function chooseExecutor(rl, current) {
|
|
|
123
241
|
}
|
|
124
242
|
return { type: "native-macos" };
|
|
125
243
|
}
|
|
126
|
-
function
|
|
127
|
-
const narrationContext =
|
|
128
|
-
?
|
|
244
|
+
function extractBridgeHandoffSummary(payload) {
|
|
245
|
+
const narrationContext = payload.narration_context && typeof payload.narration_context === "object"
|
|
246
|
+
? payload.narration_context
|
|
247
|
+
: {};
|
|
248
|
+
const plan = payload.plan && typeof payload.plan === "object"
|
|
249
|
+
? payload.plan
|
|
129
250
|
: {};
|
|
130
|
-
const
|
|
131
|
-
?
|
|
251
|
+
const job = payload.job && typeof payload.job === "object"
|
|
252
|
+
? payload.job
|
|
132
253
|
: {};
|
|
133
254
|
return normalizeText(narrationContext.summary
|
|
134
255
|
|| plan.summary
|
|
135
|
-
|| plan.assistant_message
|
|
256
|
+
|| plan.assistant_message
|
|
257
|
+
|| extractJobSummary(job));
|
|
136
258
|
}
|
|
137
259
|
function extractJobStepId(job) {
|
|
138
260
|
return normalizeText(job.runtime_current_step_id
|
|
@@ -169,13 +291,17 @@ function extractConfirmationPrompt(job) {
|
|
|
169
291
|
|| payload.kernel_confirmation_summary
|
|
170
292
|
|| payload.confirmation_message) || "O Otto está aguardando sua confirmação para continuar.";
|
|
171
293
|
}
|
|
172
|
-
function renderStatusOverview(config) {
|
|
294
|
+
function renderStatusOverview(config, runtimeSession) {
|
|
173
295
|
return [
|
|
174
296
|
`${style("Device", ANSI.blue, supportsAnsi())}: ${config.deviceName}`,
|
|
175
297
|
`${style("Device ID", ANSI.blue, supportsAnsi())}: ${config.deviceId}`,
|
|
176
298
|
`${style("API", ANSI.blue, supportsAnsi())}: ${config.apiBaseUrl}`,
|
|
177
299
|
`${style("Executor", ANSI.blue, supportsAnsi())}: ${config.executor.type}`,
|
|
178
300
|
`${style("Approval", ANSI.blue, supportsAnsi())}: ${config.approvalMode}`,
|
|
301
|
+
`${style("Runtime", ANSI.blue, supportsAnsi())}: ${runtimeSession?.getStatusLabel() || "offline"}`,
|
|
302
|
+
...(runtimeSession?.getStatusDetail()
|
|
303
|
+
? [`${style("Runtime note", ANSI.blue, supportsAnsi())}: ${runtimeSession.getStatusDetail()}`]
|
|
304
|
+
: []),
|
|
179
305
|
`${style("Config", ANSI.blue, supportsAnsi())}: ${getBridgeConfigPath()}`,
|
|
180
306
|
];
|
|
181
307
|
}
|
|
@@ -281,15 +407,10 @@ async function followConsoleJob(rl, config, jobId) {
|
|
|
281
407
|
await delay(1400);
|
|
282
408
|
}
|
|
283
409
|
}
|
|
284
|
-
async function runOttoConsole(rl, config, options) {
|
|
410
|
+
async function runOttoConsole(rl, config, runtimeSession, options) {
|
|
285
411
|
printSection("Otto Console");
|
|
286
|
-
printMuted("
|
|
287
|
-
|
|
288
|
-
let runtimeFailure = null;
|
|
289
|
-
const runtimeTask = runtime.start().catch((error) => {
|
|
290
|
-
runtimeFailure = error instanceof Error ? error.message : String(error);
|
|
291
|
-
});
|
|
292
|
-
await delay(600);
|
|
412
|
+
printMuted("Este console usa o mesmo runtime local já ligado pelo `otto-bridge`.");
|
|
413
|
+
printMuted(`Runtime: ${runtimeSession.getStatusLabel()} · ${runtimeSession.getStatusDetail()}`);
|
|
293
414
|
const sessionId = randomUUID();
|
|
294
415
|
const conversation = [];
|
|
295
416
|
const printConsoleHelp = () => {
|
|
@@ -310,7 +431,8 @@ async function runOttoConsole(rl, config, options) {
|
|
|
310
431
|
return;
|
|
311
432
|
}
|
|
312
433
|
if (normalizedPrompt === "/status") {
|
|
313
|
-
renderStatusOverview(config).forEach((line) => console.log(line));
|
|
434
|
+
renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
|
|
435
|
+
const runtimeFailure = runtimeSession.getLastError();
|
|
314
436
|
if (runtimeFailure) {
|
|
315
437
|
printWarning(`Runtime reportou erro: ${runtimeFailure}`);
|
|
316
438
|
}
|
|
@@ -319,62 +441,94 @@ async function runOttoConsole(rl, config, options) {
|
|
|
319
441
|
if (normalizedPrompt === "/exit") {
|
|
320
442
|
throw new Error("__OTTO_CONSOLE_EXIT__");
|
|
321
443
|
}
|
|
444
|
+
conversation.push({ role: "user", content: normalizedPrompt });
|
|
445
|
+
while (conversation.length > 18) {
|
|
446
|
+
conversation.shift();
|
|
447
|
+
}
|
|
322
448
|
console.log(`${style("você", ANSI.white, supportsAnsi())} ${normalizedPrompt}`);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
449
|
+
let streamedAssistant = "";
|
|
450
|
+
let assistantPrefixPrinted = false;
|
|
451
|
+
let handoffPayload = null;
|
|
452
|
+
await streamDeviceCliChat(config, {
|
|
453
|
+
messages: conversation,
|
|
326
454
|
session_id: sessionId,
|
|
327
|
-
|
|
455
|
+
}, async (event) => {
|
|
456
|
+
const chunkType = normalizeText(event.chunk_type).toLowerCase();
|
|
457
|
+
const eventType = normalizeText(event.type).toLowerCase();
|
|
458
|
+
if (chunkType === "bridge_handoff") {
|
|
459
|
+
handoffPayload = event;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (chunkType === "search_status") {
|
|
463
|
+
const status = normalizeText(event.status);
|
|
464
|
+
if (status) {
|
|
465
|
+
printMuted(`Busca: ${status}`);
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const errorMessage = normalizeText(event.error || (eventType === "error" ? event.content : ""));
|
|
470
|
+
if (errorMessage) {
|
|
471
|
+
throw new Error(errorMessage);
|
|
472
|
+
}
|
|
473
|
+
const contentChunk = typeof event.content === "string" ? event.content : "";
|
|
474
|
+
if (!contentChunk) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!assistantPrefixPrinted) {
|
|
478
|
+
output.write(`${style("otto", ANSI.coral, supportsAnsi())} `);
|
|
479
|
+
assistantPrefixPrinted = true;
|
|
480
|
+
}
|
|
481
|
+
output.write(contentChunk);
|
|
482
|
+
streamedAssistant += contentChunk;
|
|
328
483
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
console.log(`${style("otto", ANSI.coral, supportsAnsi())} ${summary}`);
|
|
332
|
-
}
|
|
333
|
-
conversation.push({ role: "user", content: normalizedPrompt });
|
|
334
|
-
if (summary) {
|
|
335
|
-
conversation.push({ role: "assistant", content: summary });
|
|
336
|
-
}
|
|
337
|
-
while (conversation.length > 16) {
|
|
338
|
-
conversation.shift();
|
|
484
|
+
if (assistantPrefixPrinted) {
|
|
485
|
+
output.write("\n");
|
|
339
486
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
if (
|
|
345
|
-
|
|
487
|
+
let finalAssistantSummary = normalizeText(streamedAssistant);
|
|
488
|
+
if (handoffPayload) {
|
|
489
|
+
const handoffData = handoffPayload;
|
|
490
|
+
const bridgeSummary = extractBridgeHandoffSummary(handoffData);
|
|
491
|
+
if (bridgeSummary) {
|
|
492
|
+
console.log(`${style("otto", ANSI.coral, supportsAnsi())} ${bridgeSummary}`);
|
|
493
|
+
}
|
|
494
|
+
const job = handoffData.job && typeof handoffData.job === "object"
|
|
495
|
+
? handoffData.job
|
|
496
|
+
: null;
|
|
497
|
+
const jobId = normalizeText(job?.id);
|
|
498
|
+
if (jobId) {
|
|
499
|
+
finalAssistantSummary = await followConsoleJob(rl, config, jobId);
|
|
500
|
+
}
|
|
501
|
+
else if (bridgeSummary) {
|
|
502
|
+
finalAssistantSummary = bridgeSummary;
|
|
346
503
|
}
|
|
347
504
|
}
|
|
348
|
-
|
|
349
|
-
|
|
505
|
+
if (finalAssistantSummary) {
|
|
506
|
+
conversation.push({ role: "assistant", content: finalAssistantSummary });
|
|
507
|
+
while (conversation.length > 18) {
|
|
508
|
+
conversation.shift();
|
|
509
|
+
}
|
|
350
510
|
}
|
|
351
511
|
};
|
|
352
512
|
printConsoleHelp();
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
513
|
+
if (options?.initialPrompt) {
|
|
514
|
+
await handlePrompt(options.initialPrompt);
|
|
515
|
+
}
|
|
516
|
+
for (;;) {
|
|
517
|
+
const promptText = await ask(rl, "OTTO", { allowEmpty: true });
|
|
518
|
+
try {
|
|
519
|
+
await handlePrompt(promptText);
|
|
356
520
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
await handlePrompt(promptText);
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
if (error instanceof Error && error.message === "__OTTO_CONSOLE_EXIT__") {
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
throw error;
|
|
521
|
+
catch (error) {
|
|
522
|
+
if (error instanceof Error && error.message === "__OTTO_CONSOLE_EXIT__") {
|
|
523
|
+
break;
|
|
367
524
|
}
|
|
525
|
+
throw error;
|
|
368
526
|
}
|
|
369
527
|
}
|
|
370
|
-
finally {
|
|
371
|
-
await runtime.stop().catch(() => undefined);
|
|
372
|
-
await runtimeTask.catch(() => undefined);
|
|
373
|
-
}
|
|
374
528
|
}
|
|
375
|
-
async function printStatusView(rl, config) {
|
|
529
|
+
async function printStatusView(rl, config, runtimeSession) {
|
|
376
530
|
printSection("Bridge Status");
|
|
377
|
-
renderStatusOverview(config).forEach((line) => console.log(line));
|
|
531
|
+
renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
|
|
378
532
|
await printExtensionsOverview(config);
|
|
379
533
|
await pauseForEnter(rl);
|
|
380
534
|
}
|
|
@@ -409,6 +563,7 @@ async function pickHomeChoice(rl, paired) {
|
|
|
409
563
|
}
|
|
410
564
|
export async function launchInteractiveCli(options) {
|
|
411
565
|
const rl = await createPromptInterface();
|
|
566
|
+
let runtimeSession = null;
|
|
412
567
|
try {
|
|
413
568
|
console.clear();
|
|
414
569
|
console.log(renderBanner());
|
|
@@ -417,16 +572,19 @@ export async function launchInteractiveCli(options) {
|
|
|
417
572
|
const setup = await runSetupWizard(rl, options);
|
|
418
573
|
config = setup.config;
|
|
419
574
|
if (config && setup.openConsole) {
|
|
420
|
-
|
|
421
|
-
|
|
575
|
+
runtimeSession = new CliRuntimeSession(config);
|
|
576
|
+
await runtimeSession.ensureStarted();
|
|
577
|
+
await runOttoConsole(rl, config, runtimeSession);
|
|
422
578
|
}
|
|
423
579
|
if (!config) {
|
|
424
580
|
return;
|
|
425
581
|
}
|
|
426
582
|
}
|
|
583
|
+
runtimeSession = runtimeSession || new CliRuntimeSession(config);
|
|
584
|
+
await runtimeSession.ensureStarted();
|
|
427
585
|
for (;;) {
|
|
428
586
|
console.log("");
|
|
429
|
-
renderStatusOverview(config).forEach((line) => console.log(line));
|
|
587
|
+
renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
|
|
430
588
|
const choice = await pickHomeChoice(rl, true);
|
|
431
589
|
if (choice === "exit") {
|
|
432
590
|
break;
|
|
@@ -435,18 +593,25 @@ export async function launchInteractiveCli(options) {
|
|
|
435
593
|
const setup = await runSetupWizard(rl);
|
|
436
594
|
if (setup.config) {
|
|
437
595
|
config = setup.config;
|
|
596
|
+
if (runtimeSession) {
|
|
597
|
+
await runtimeSession.replaceConfig(setup.config);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
runtimeSession = new CliRuntimeSession(setup.config);
|
|
601
|
+
await runtimeSession.ensureStarted();
|
|
602
|
+
}
|
|
438
603
|
}
|
|
439
|
-
if (setup.config && setup.openConsole) {
|
|
440
|
-
await runOttoConsole(rl, setup.config);
|
|
604
|
+
if (setup.config && setup.openConsole && runtimeSession) {
|
|
605
|
+
await runOttoConsole(rl, setup.config, runtimeSession);
|
|
441
606
|
}
|
|
442
607
|
continue;
|
|
443
608
|
}
|
|
444
|
-
if (choice === "console") {
|
|
445
|
-
await runOttoConsole(rl, config);
|
|
609
|
+
if (choice === "console" && runtimeSession) {
|
|
610
|
+
await runOttoConsole(rl, config, runtimeSession);
|
|
446
611
|
continue;
|
|
447
612
|
}
|
|
448
|
-
if (choice === "status") {
|
|
449
|
-
await printStatusView(rl, config);
|
|
613
|
+
if (choice === "status" && runtimeSession) {
|
|
614
|
+
await printStatusView(rl, config, runtimeSession);
|
|
450
615
|
continue;
|
|
451
616
|
}
|
|
452
617
|
if (choice === "extensions") {
|
|
@@ -456,35 +621,48 @@ export async function launchInteractiveCli(options) {
|
|
|
456
621
|
}
|
|
457
622
|
}
|
|
458
623
|
finally {
|
|
624
|
+
await runtimeSession?.stop().catch(() => undefined);
|
|
459
625
|
rl.close();
|
|
460
626
|
}
|
|
461
627
|
}
|
|
462
628
|
export async function runSetupCommand(options) {
|
|
463
629
|
const rl = await createPromptInterface();
|
|
630
|
+
let runtimeSession = null;
|
|
464
631
|
try {
|
|
465
632
|
console.clear();
|
|
466
633
|
console.log(renderBanner());
|
|
467
634
|
const setup = await runSetupWizard(rl, options);
|
|
468
635
|
if (setup.config && setup.openConsole) {
|
|
469
|
-
|
|
636
|
+
runtimeSession = new CliRuntimeSession(setup.config);
|
|
637
|
+
await runtimeSession.ensureStarted();
|
|
638
|
+
await runOttoConsole(rl, setup.config, runtimeSession);
|
|
470
639
|
}
|
|
471
640
|
}
|
|
472
641
|
finally {
|
|
642
|
+
await runtimeSession?.stop().catch(() => undefined);
|
|
473
643
|
rl.close();
|
|
474
644
|
}
|
|
475
645
|
}
|
|
476
646
|
export async function runConsoleCommand(initialPrompt) {
|
|
477
|
-
const config = await loadBridgeConfig();
|
|
478
|
-
if (!config) {
|
|
479
|
-
throw new Error("Nenhum pairing local encontrado. Rode `otto-bridge setup` primeiro.");
|
|
480
|
-
}
|
|
481
647
|
const rl = await createPromptInterface();
|
|
648
|
+
let runtimeSession = null;
|
|
482
649
|
try {
|
|
483
650
|
console.clear();
|
|
484
651
|
console.log(renderBanner());
|
|
485
|
-
|
|
652
|
+
let config = await loadBridgeConfig();
|
|
653
|
+
if (!config) {
|
|
654
|
+
const setup = await runSetupWizard(rl);
|
|
655
|
+
if (!setup.config) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
config = setup.config;
|
|
659
|
+
}
|
|
660
|
+
runtimeSession = new CliRuntimeSession(config);
|
|
661
|
+
await runtimeSession.ensureStarted();
|
|
662
|
+
await runOttoConsole(rl, config, runtimeSession, { initialPrompt });
|
|
486
663
|
}
|
|
487
664
|
finally {
|
|
665
|
+
await runtimeSession?.stop().catch(() => undefined);
|
|
488
666
|
rl.close();
|
|
489
667
|
}
|
|
490
668
|
}
|
|
@@ -236,6 +236,7 @@ function isSupportedBridgeAutomation(automation) {
|
|
|
236
236
|
}
|
|
237
237
|
export class LocalAutomationRuntime {
|
|
238
238
|
config;
|
|
239
|
+
logger;
|
|
239
240
|
automations = new Map();
|
|
240
241
|
states = new Map();
|
|
241
242
|
syncTimer = null;
|
|
@@ -245,8 +246,12 @@ export class LocalAutomationRuntime {
|
|
|
245
246
|
started = false;
|
|
246
247
|
stopped = false;
|
|
247
248
|
whatsappBrowser = null;
|
|
248
|
-
constructor(config) {
|
|
249
|
+
constructor(config, logger) {
|
|
249
250
|
this.config = config;
|
|
251
|
+
this.logger = logger;
|
|
252
|
+
}
|
|
253
|
+
logWarn(message) {
|
|
254
|
+
(this.logger?.warn || console.warn)(message);
|
|
250
255
|
}
|
|
251
256
|
async start() {
|
|
252
257
|
if (this.started) {
|
|
@@ -256,11 +261,11 @@ export class LocalAutomationRuntime {
|
|
|
256
261
|
this.stopped = false;
|
|
257
262
|
await this.syncAutomations().catch((error) => {
|
|
258
263
|
const detail = error instanceof Error ? error.message : String(error);
|
|
259
|
-
|
|
264
|
+
this.logWarn(`[otto-bridge] local automations sync failed: ${detail}`);
|
|
260
265
|
});
|
|
261
266
|
await this.tick().catch((error) => {
|
|
262
267
|
const detail = error instanceof Error ? error.message : String(error);
|
|
263
|
-
|
|
268
|
+
this.logWarn(`[otto-bridge] local automations tick failed: ${detail}`);
|
|
264
269
|
});
|
|
265
270
|
this.syncTimer = setInterval(() => {
|
|
266
271
|
void this.syncAutomations();
|
|
@@ -332,7 +337,7 @@ export class LocalAutomationRuntime {
|
|
|
332
337
|
}
|
|
333
338
|
catch (error) {
|
|
334
339
|
const detail = error instanceof Error ? error.message : String(error);
|
|
335
|
-
|
|
340
|
+
this.logWarn(`[otto-bridge] local automations sync failed: ${detail}`);
|
|
336
341
|
}
|
|
337
342
|
finally {
|
|
338
343
|
this.syncInFlight = false;
|
|
@@ -368,7 +373,7 @@ export class LocalAutomationRuntime {
|
|
|
368
373
|
}
|
|
369
374
|
catch (error) {
|
|
370
375
|
const detail = error instanceof Error ? error.message : String(error);
|
|
371
|
-
|
|
376
|
+
this.logWarn(`[otto-bridge] local automation failed id=${automationId}: ${detail}`);
|
|
372
377
|
}
|
|
373
378
|
finally {
|
|
374
379
|
state.running = false;
|
|
@@ -401,7 +406,7 @@ export class LocalAutomationRuntime {
|
|
|
401
406
|
await browser.ensureReady();
|
|
402
407
|
const selected = await browser.selectConversation(contact);
|
|
403
408
|
if (!selected) {
|
|
404
|
-
|
|
409
|
+
this.logWarn(`[otto-bridge] local whatsapp automation could not find contact="${contact}"`);
|
|
405
410
|
return;
|
|
406
411
|
}
|
|
407
412
|
await this.processWhatsAppConversation(automation, state, browser, contact, { alreadySelected: true });
|
|
@@ -430,7 +435,7 @@ export class LocalAutomationRuntime {
|
|
|
430
435
|
try {
|
|
431
436
|
const selected = await browser.selectConversation(contact);
|
|
432
437
|
if (!selected) {
|
|
433
|
-
|
|
438
|
+
this.logWarn(`[otto-bridge] local whatsapp inbox automation could not find contact="${contact}"`);
|
|
434
439
|
continue;
|
|
435
440
|
}
|
|
436
441
|
await this.processWhatsAppConversation(automation, state, browser, contact, {
|
|
@@ -440,7 +445,7 @@ export class LocalAutomationRuntime {
|
|
|
440
445
|
}
|
|
441
446
|
catch (error) {
|
|
442
447
|
const detail = error instanceof Error ? error.message : String(error);
|
|
443
|
-
|
|
448
|
+
this.logWarn(`[otto-bridge] local whatsapp inbox conversation failed contact="${contact}": ${detail}`);
|
|
444
449
|
}
|
|
445
450
|
}
|
|
446
451
|
}
|
|
@@ -495,7 +500,7 @@ export class LocalAutomationRuntime {
|
|
|
495
500
|
if (!options?.alreadySelected) {
|
|
496
501
|
const selected = await browser.selectConversation(contact);
|
|
497
502
|
if (!selected) {
|
|
498
|
-
|
|
503
|
+
this.logWarn(`[otto-bridge] local whatsapp automation could not find contact="${contact}"`);
|
|
499
504
|
return;
|
|
500
505
|
}
|
|
501
506
|
}
|
|
@@ -569,7 +574,7 @@ export class LocalAutomationRuntime {
|
|
|
569
574
|
}
|
|
570
575
|
catch (error) {
|
|
571
576
|
const detail = error instanceof Error ? error.message : String(error);
|
|
572
|
-
|
|
577
|
+
this.logWarn(`[otto-bridge] local whatsapp completion failed id=${automation.id}: ${detail}`);
|
|
573
578
|
this.rememberDeltaHash(automation, state, contact, sent ? completionDeltaHash : deltaHash);
|
|
574
579
|
}
|
|
575
580
|
}
|
package/dist/main.js
CHANGED
|
@@ -110,7 +110,6 @@ function printUsage() {
|
|
|
110
110
|
otto-bridge setup
|
|
111
111
|
otto-bridge console
|
|
112
112
|
otto-bridge pair --api http://localhost:8000 --code ABC123 [--name "Meu PC"] [--executor native-macos|mock|clawd-cursor]
|
|
113
|
-
otto-bridge run [--executor native-macos|mock|clawd-cursor] [--clawd-url http://127.0.0.1:3847]
|
|
114
113
|
otto-bridge status
|
|
115
114
|
otto-bridge extensions --list
|
|
116
115
|
otto-bridge extensions --install github
|
|
@@ -126,7 +125,6 @@ Examples:
|
|
|
126
125
|
otto-bridge setup
|
|
127
126
|
otto-bridge console
|
|
128
127
|
otto-bridge pair --api https://api.leg3ndy.com.br --code ABC123
|
|
129
|
-
otto-bridge run
|
|
130
128
|
otto-bridge extensions --install whatsappweb
|
|
131
129
|
otto-bridge extensions --setup whatsappweb
|
|
132
130
|
otto-bridge extensions --status whatsappweb
|
|
@@ -287,7 +285,7 @@ async function runPairCommand(args) {
|
|
|
287
285
|
console.log(`[otto-bridge] paired device=${config.deviceId}`);
|
|
288
286
|
console.log(`[otto-bridge] executor=${config.executor.type}`);
|
|
289
287
|
console.log(`[otto-bridge] config=${getBridgeConfigPath()}`);
|
|
290
|
-
console.log("[otto-bridge] next step: run `otto-bridge
|
|
288
|
+
console.log("[otto-bridge] next step: run `otto-bridge` to abrir o hub e manter o runtime local online");
|
|
291
289
|
}
|
|
292
290
|
async function loadRequiredBridgeConfig() {
|
|
293
291
|
const config = await loadBridgeConfig();
|
|
@@ -297,6 +295,7 @@ async function loadRequiredBridgeConfig() {
|
|
|
297
295
|
return config;
|
|
298
296
|
}
|
|
299
297
|
async function runRuntimeCommand(args) {
|
|
298
|
+
console.log("[otto-bridge] `run` agora é um alias legado. Prefira `otto-bridge`.");
|
|
300
299
|
const config = await loadRequiredBridgeConfig();
|
|
301
300
|
const runtimeConfig = {
|
|
302
301
|
...config,
|
package/dist/runtime.js
CHANGED
|
@@ -78,6 +78,7 @@ async function parseSocketMessage(data) {
|
|
|
78
78
|
}
|
|
79
79
|
export class BridgeRuntime {
|
|
80
80
|
config;
|
|
81
|
+
options;
|
|
81
82
|
reconnectDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
82
83
|
executor;
|
|
83
84
|
localAutomationRuntime;
|
|
@@ -87,10 +88,23 @@ export class BridgeRuntime {
|
|
|
87
88
|
started = false;
|
|
88
89
|
pendingConfirmations = new Map();
|
|
89
90
|
activeCancels = new Map();
|
|
90
|
-
constructor(config, executor) {
|
|
91
|
+
constructor(config, executor, options = {}) {
|
|
91
92
|
this.config = config;
|
|
93
|
+
this.options = options;
|
|
92
94
|
this.executor = executor ?? this.createDefaultExecutor(config);
|
|
93
|
-
this.localAutomationRuntime = new LocalAutomationRuntime(config);
|
|
95
|
+
this.localAutomationRuntime = new LocalAutomationRuntime(config, this.options.logger);
|
|
96
|
+
}
|
|
97
|
+
logInfo(message) {
|
|
98
|
+
(this.options.logger?.info || console.log)(message);
|
|
99
|
+
}
|
|
100
|
+
logWarn(message) {
|
|
101
|
+
(this.options.logger?.warn || console.warn)(message);
|
|
102
|
+
}
|
|
103
|
+
logError(message) {
|
|
104
|
+
(this.options.logger?.error || console.error)(message);
|
|
105
|
+
}
|
|
106
|
+
emit(event) {
|
|
107
|
+
this.options.logger?.event?.(event);
|
|
94
108
|
}
|
|
95
109
|
async buildHelloMetadata() {
|
|
96
110
|
const metadata = {
|
|
@@ -169,10 +183,11 @@ export class BridgeRuntime {
|
|
|
169
183
|
}
|
|
170
184
|
await this.localAutomationRuntime.start().catch((error) => {
|
|
171
185
|
const detail = error instanceof Error ? error.message : String(error);
|
|
172
|
-
|
|
186
|
+
this.logError(`[otto-bridge] local automation runtime failed to start: ${detail}`);
|
|
173
187
|
});
|
|
174
188
|
}
|
|
175
|
-
|
|
189
|
+
this.logInfo(`[otto-bridge] runtime start device=${this.config.deviceId}`);
|
|
190
|
+
this.emit({ type: "starting", deviceId: this.config.deviceId });
|
|
176
191
|
while (!this.stopped) {
|
|
177
192
|
try {
|
|
178
193
|
await this.connectOnce();
|
|
@@ -183,12 +198,14 @@ export class BridgeRuntime {
|
|
|
183
198
|
break;
|
|
184
199
|
}
|
|
185
200
|
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
-
|
|
201
|
+
this.logError(`[otto-bridge] socket error: ${message}`);
|
|
202
|
+
this.emit({ type: "socket_error", message });
|
|
187
203
|
}
|
|
188
204
|
if (this.stopped) {
|
|
189
205
|
break;
|
|
190
206
|
}
|
|
191
|
-
|
|
207
|
+
this.logInfo(`[otto-bridge] reconnecting in ${this.reconnectDelayMs}ms`);
|
|
208
|
+
this.emit({ type: "reconnecting", delayMs: this.reconnectDelayMs });
|
|
192
209
|
await delay(this.reconnectDelayMs);
|
|
193
210
|
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, DEFAULT_RECONNECT_MAX_DELAY_MS);
|
|
194
211
|
}
|
|
@@ -238,10 +255,11 @@ export class BridgeRuntime {
|
|
|
238
255
|
};
|
|
239
256
|
return await new Promise((resolve, reject) => {
|
|
240
257
|
socket.addEventListener("open", () => {
|
|
241
|
-
|
|
258
|
+
this.logInfo(`[otto-bridge] connected ws=${this.config.wsUrl}`);
|
|
259
|
+
this.emit({ type: "connected", wsUrl: this.config.wsUrl });
|
|
242
260
|
this.sendHello(socket).catch((error) => {
|
|
243
261
|
const detail = error instanceof Error ? error.message : String(error);
|
|
244
|
-
|
|
262
|
+
this.logError(`[otto-bridge] hello metadata failed: ${detail}`);
|
|
245
263
|
});
|
|
246
264
|
heartbeatTimer = setInterval(() => {
|
|
247
265
|
if (socket.readyState === WebSocket.OPEN) {
|
|
@@ -260,14 +278,15 @@ export class BridgeRuntime {
|
|
|
260
278
|
}
|
|
261
279
|
catch (error) {
|
|
262
280
|
const detail = error instanceof Error ? error.message : String(error);
|
|
263
|
-
|
|
281
|
+
this.logError(`[otto-bridge] invalid message: ${detail}`);
|
|
264
282
|
}
|
|
265
283
|
});
|
|
266
284
|
socket.addEventListener("close", (event) => {
|
|
267
285
|
stopHeartbeat();
|
|
268
286
|
rejectPendingConfirmations(new Error("WebSocket closed while awaiting confirmation"));
|
|
269
287
|
this.activeSocket = null;
|
|
270
|
-
|
|
288
|
+
this.logInfo(`[otto-bridge] socket closed code=${event.code}`);
|
|
289
|
+
this.emit({ type: "socket_closed", code: event.code });
|
|
271
290
|
resolve();
|
|
272
291
|
});
|
|
273
292
|
socket.addEventListener("error", () => {
|
|
@@ -288,14 +307,16 @@ export class BridgeRuntime {
|
|
|
288
307
|
const type = String(message.type || "");
|
|
289
308
|
switch (type) {
|
|
290
309
|
case "device.hello":
|
|
291
|
-
|
|
310
|
+
this.logInfo(`[otto-bridge] server hello device=${String(message.device_id || "")}`);
|
|
311
|
+
this.emit({ type: "server_hello", deviceId: String(message.device_id || "") });
|
|
292
312
|
return;
|
|
293
313
|
case "device.hello_ack":
|
|
294
314
|
this.maybeLogBridgeReleaseNotice(message);
|
|
295
315
|
case "device.heartbeat_ack":
|
|
296
316
|
return;
|
|
297
317
|
case "device.job.start":
|
|
298
|
-
|
|
318
|
+
this.logInfo(`[otto-bridge] job start payload=${JSON.stringify(message)}`);
|
|
319
|
+
this.emit({ type: "job_start", jobId: String(message.job_id || "") });
|
|
299
320
|
this.executeJob(socket, {
|
|
300
321
|
job_id: String(message.job_id || ""),
|
|
301
322
|
device_id: String(message.device_id || ""),
|
|
@@ -305,7 +326,7 @@ export class BridgeRuntime {
|
|
|
305
326
|
: {},
|
|
306
327
|
}).catch((error) => {
|
|
307
328
|
const detail = error instanceof Error ? error.message : String(error);
|
|
308
|
-
|
|
329
|
+
this.logError(`[otto-bridge] executor error: ${detail}`);
|
|
309
330
|
});
|
|
310
331
|
return;
|
|
311
332
|
case "device.job.confirmation":
|
|
@@ -315,7 +336,7 @@ export class BridgeRuntime {
|
|
|
315
336
|
await this.cancelJob(String(message.job_id || ""), String(message.step_id || ""));
|
|
316
337
|
return;
|
|
317
338
|
default:
|
|
318
|
-
|
|
339
|
+
this.logInfo(`[otto-bridge] event=${type || "unknown"} payload=${JSON.stringify(message)}`);
|
|
319
340
|
}
|
|
320
341
|
}
|
|
321
342
|
maybeLogBridgeReleaseNotice(message) {
|
|
@@ -334,11 +355,15 @@ export class BridgeRuntime {
|
|
|
334
355
|
}
|
|
335
356
|
this.lastBridgeReleaseNoticeKey = noticeKey;
|
|
336
357
|
if (updateRequired) {
|
|
337
|
-
|
|
358
|
+
const message = `[otto-bridge] update required current=${this.config.bridgeVersion} min_supported=${minSupportedVersion || "unknown"} latest=${latestVersion || "unknown"} command="${updateCommand}"`;
|
|
359
|
+
this.logWarn(message);
|
|
360
|
+
this.emit({ type: "update_required", message });
|
|
338
361
|
return;
|
|
339
362
|
}
|
|
340
363
|
if (updateAvailable) {
|
|
341
|
-
|
|
364
|
+
const message = `[otto-bridge] update available current=${this.config.bridgeVersion} latest=${latestVersion || "unknown"} command="${updateCommand}"`;
|
|
365
|
+
this.logInfo(message);
|
|
366
|
+
this.emit({ type: "update_available", message });
|
|
342
367
|
}
|
|
343
368
|
}
|
|
344
369
|
clearPendingConfirmations(jobId) {
|
|
@@ -382,7 +407,7 @@ export class BridgeRuntime {
|
|
|
382
407
|
const waiter = this.pendingConfirmations.get(confirmationKey(jobId, stepId))
|
|
383
408
|
|| this.pendingConfirmations.get(jobId);
|
|
384
409
|
if (!jobId || !waiter) {
|
|
385
|
-
|
|
410
|
+
this.logWarn(`[otto-bridge] unexpected confirmation payload=${JSON.stringify(message)}`);
|
|
386
411
|
return;
|
|
387
412
|
}
|
|
388
413
|
if (action !== "approve" && action !== "reject") {
|
|
@@ -408,16 +433,16 @@ export class BridgeRuntime {
|
|
|
408
433
|
return;
|
|
409
434
|
}
|
|
410
435
|
if (this.resolvePendingCancellation(jobId, stepId)) {
|
|
411
|
-
|
|
436
|
+
this.logInfo(`[otto-bridge] confirmation cancelled job=${jobId}${stepId ? ` step=${stepId}` : ""}`);
|
|
412
437
|
return;
|
|
413
438
|
}
|
|
414
439
|
const cancel = this.activeCancels.get(jobId);
|
|
415
440
|
if (!cancel) {
|
|
416
|
-
|
|
441
|
+
this.logWarn(`[otto-bridge] cancel requested for unknown job=${jobId}`);
|
|
417
442
|
return;
|
|
418
443
|
}
|
|
419
444
|
if (stepId) {
|
|
420
|
-
|
|
445
|
+
this.logInfo(`[otto-bridge] cancel requested job=${jobId} step=${stepId}`);
|
|
421
446
|
}
|
|
422
447
|
await cancel();
|
|
423
448
|
}
|
|
@@ -441,7 +466,8 @@ export class BridgeRuntime {
|
|
|
441
466
|
if (typeof this.executor.cancel === "function") {
|
|
442
467
|
await this.executor.cancel(job.job_id);
|
|
443
468
|
}
|
|
444
|
-
|
|
469
|
+
this.logInfo(`[otto-bridge] job cancelled job_id=${job.job_id}`);
|
|
470
|
+
this.emit({ type: "job_cancelled", jobId: job.job_id });
|
|
445
471
|
});
|
|
446
472
|
try {
|
|
447
473
|
await this.executor.run(job, {
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "1.0.
|
|
2
|
+
export const BRIDGE_VERSION = "1.0.1";
|
|
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
package/scripts/postinstall.mjs
CHANGED
|
@@ -24,7 +24,7 @@ if (!existsSync(mainPath)) {
|
|
|
24
24
|
process.exit(0);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
console.log("\n[otto-bridge] Welcome to OTTOAI 1.0.
|
|
27
|
+
console.log("\n[otto-bridge] Welcome to OTTOAI 1.0.1");
|
|
28
28
|
console.log("[otto-bridge] Vamos iniciar o setup interativo do bridge.\n");
|
|
29
29
|
|
|
30
30
|
const result = spawnSync(process.execPath, [mainPath, "setup", "--postinstall"], {
|