@leg3ndy/otto-bridge 1.0.1 → 1.0.3

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
@@ -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.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).
18
+ Para a release atual `1.0.3`, com console terminal azul da marca, seletor `Fast/Thinking` e rendering estruturado de resultados locais, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_3_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_3_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.1.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.0.3.tgz
42
42
  ```
43
43
 
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.
44
+ Na linha `1.0.3`, `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.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`.
46
+ No macOS, a linha `1.0.3` 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` 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.
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.3` consolida o hub terminal como fluxo principal, esconde aliases legados da superfície pública, alinha o console ao comportamento normal do Otto antes de fazer handoff local e melhora a leitura visual/estruturada do resultado no proprio terminal.
49
49
 
50
50
  ## Publicacao
51
51
 
@@ -80,7 +80,6 @@ otto-bridge --help
80
80
 
81
81
  ```bash
82
82
  otto-bridge
83
- otto-bridge home
84
83
  ```
85
84
 
86
85
  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.
@@ -116,7 +115,7 @@ No macOS, o caminho recomendado agora e o executor nativo do Otto Bridge. Se nen
116
115
  otto-bridge
117
116
  ```
118
117
 
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.
118
+ 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 nenhum comando separado de runtime.
120
119
 
121
120
  Se precisar do executor nativo no macOS sem reparar, atualize o pairing/config e reabra o hub:
122
121
 
@@ -124,8 +123,6 @@ Se precisar do executor nativo no macOS sem reparar, atualize o pairing/config e
124
123
  otto-bridge setup
125
124
  ```
126
125
 
127
- `otto-bridge run` continua existindo apenas como alias legado/headless para compatibilidade operacional.
128
-
129
126
  O adapter `clawd-cursor` continua disponivel como override opcional no pairing legado:
130
127
 
131
128
  ```bash
@@ -140,9 +137,17 @@ otto-bridge console
140
137
 
141
138
  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.
142
139
 
140
+ Dentro do console, use:
141
+
142
+ - `/model fast` para `OttoAI Fast`
143
+ - `/model thinking` para `OttoAI Thinking`
144
+ - `/status` para ver detalhes técnicos do bridge e do runtime
145
+
146
+ Quando o handoff local devolver resultado estruturado, o CLI agora mostra inline a listagem de arquivos e o conteúdo de `read_file`, em vez de só resumir que executou a tarefa.
147
+
143
148
  ### WhatsApp Web em background
144
149
 
145
- Fluxo recomendado na linha `1.0.1`:
150
+ Fluxo recomendado na linha `1.0.3`:
146
151
 
147
152
  ```bash
148
153
  otto-bridge extensions --install whatsappweb
@@ -152,13 +157,13 @@ otto-bridge extensions --status whatsappweb
152
157
 
153
158
  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.
154
159
 
155
- Contrato da linha `1.0.1`:
160
+ Contrato da linha `1.0.3`:
156
161
 
157
162
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
158
163
  - `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
164
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
160
165
 
161
- ## Handoff rapido da linha 1.0.1
166
+ ## Handoff rapido da linha 1.0.3
162
167
 
163
168
  Ja fechado no codigo:
164
169
 
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createInterface } from "node:readline/promises";
3
- import { stdin as input, stdout as output } from "node:process";
3
+ import process, { stdin as input, stdout as output } from "node:process";
4
4
  import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
5
5
  import { streamDeviceCliChat, } from "./chat_cli_client.js";
6
6
  import { formatManagedBridgeExtensionStatus, isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
@@ -12,9 +12,12 @@ const ANSI = {
12
12
  reset: "\u001b[0m",
13
13
  dim: "\u001b[2m",
14
14
  bold: "\u001b[1m",
15
- coral: "\u001b[38;5;216m",
15
+ italic: "\u001b[3m",
16
+ brandBlue: "\u001b[38;2;0;119;208m",
16
17
  blue: "\u001b[38;5;111m",
17
18
  teal: "\u001b[38;5;80m",
19
+ slate: "\u001b[38;5;245m",
20
+ slateItalic: "\u001b[38;5;245m\u001b[3m",
18
21
  amber: "\u001b[38;5;221m",
19
22
  red: "\u001b[38;5;203m",
20
23
  green: "\u001b[38;5;114m",
@@ -28,6 +31,23 @@ const OTTOAI_BANNER = [
28
31
  "╚██████╔╝ ██║ ██║ ╚██████╔╝ ██║ ██║██║",
29
32
  " ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝",
30
33
  ];
34
+ const CLI_EXIT_SENTINEL = "__OTTO_BRIDGE_CLI_EXIT__";
35
+ const CLI_MODEL_REGISTRY = {
36
+ fast: {
37
+ label: "OttoAI Fast",
38
+ requestModel: "deepseek-chat",
39
+ aliases: ["fast", "chat", "default", "ottoai fast", "otto fast"],
40
+ },
41
+ thinking: {
42
+ label: "OttoAI Thinking",
43
+ requestModel: "deepseek-reasoner",
44
+ aliases: ["thinking", "reasoner", "think", "ottoai thinking", "otto thinking"],
45
+ },
46
+ };
47
+ const MAX_RENDERED_LIST_ENTRIES = 28;
48
+ const MAX_RENDERED_LIST_ENTRIES_COMPACT = 10;
49
+ const MAX_RENDERED_FILE_CHARS = 6_000;
50
+ const MAX_RENDERED_FILE_CHARS_COMPACT = 1_400;
31
51
  class CliRuntimeSession {
32
52
  config;
33
53
  runtime = null;
@@ -156,23 +176,39 @@ function supportsAnsi() {
156
176
  }
157
177
  function renderBanner() {
158
178
  const enabled = supportsAnsi();
159
- const lines = OTTOAI_BANNER.map((line) => style(line, ANSI.coral, enabled));
179
+ const lines = OTTOAI_BANNER.map((line) => style(line, ANSI.brandBlue, enabled));
160
180
  const title = `${BRIDGE_PACKAGE_NAME} v${BRIDGE_VERSION}`;
161
- const subtitle = "Terminal bridge, pairing wizard and local Otto console";
181
+ const subtitle = "Paired local runtime and terminal console";
162
182
  return [
163
183
  lines.join("\n"),
164
184
  "",
165
- `${style("OTTO BRIDGE", ANSI.blue, enabled)} ${style(title, ANSI.white, enabled)}`,
166
- `${style(subtitle, ANSI.dim, enabled)}`,
185
+ `${style("OTTO BRIDGE", ANSI.brandBlue, enabled)} ${style(title, ANSI.white, enabled)}`,
186
+ `${style(subtitle, ANSI.slate, enabled)}`,
167
187
  ].join("\n");
168
188
  }
189
+ function createCliExitError() {
190
+ return new Error(CLI_EXIT_SENTINEL);
191
+ }
192
+ function isCliExitError(error) {
193
+ return error instanceof Error && error.message === CLI_EXIT_SENTINEL;
194
+ }
195
+ function isReadlineClosedError(error) {
196
+ const detail = error instanceof Error ? error.message : String(error || "");
197
+ const normalized = detail.toLowerCase();
198
+ return normalized.includes("readline was closed")
199
+ || normalized.includes("canceled")
200
+ || normalized.includes("aborted");
201
+ }
169
202
  function printSection(title) {
170
203
  const enabled = supportsAnsi();
171
- console.log(`\n${style(title, ANSI.blue, enabled)}`);
204
+ console.log(`\n${style(title, ANSI.brandBlue, enabled)}`);
172
205
  }
173
206
  function printMuted(message) {
174
207
  console.log(style(message, ANSI.dim, supportsAnsi()));
175
208
  }
209
+ function printSoft(message) {
210
+ console.log(style(message, ANSI.slateItalic, supportsAnsi()));
211
+ }
176
212
  function printSuccess(message) {
177
213
  console.log(style(message, ANSI.green, supportsAnsi()));
178
214
  }
@@ -192,20 +228,69 @@ function truncate(text, max = 180) {
192
228
  }
193
229
  return `${value.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
194
230
  }
231
+ function humanFileSize(value) {
232
+ const size = typeof value === "number" ? value : Number(value);
233
+ if (!Number.isFinite(size) || size < 0) {
234
+ return "";
235
+ }
236
+ if (size < 1024) {
237
+ return `${size} B`;
238
+ }
239
+ if (size < 1024 * 1024) {
240
+ return `${(size / 1024).toFixed(1)} KB`;
241
+ }
242
+ if (size < 1024 * 1024 * 1024) {
243
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
244
+ }
245
+ return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
246
+ }
247
+ function getCliModelLabel(mode) {
248
+ return CLI_MODEL_REGISTRY[mode].label;
249
+ }
250
+ function getCliModelRequestModel(mode) {
251
+ return CLI_MODEL_REGISTRY[mode].requestModel;
252
+ }
253
+ export function resolveCliModelMode(value) {
254
+ const normalized = normalizeText(value).toLowerCase();
255
+ if (!normalized) {
256
+ return null;
257
+ }
258
+ for (const [mode, definition] of Object.entries(CLI_MODEL_REGISTRY)) {
259
+ if (definition.aliases.includes(normalized)) {
260
+ return mode;
261
+ }
262
+ }
263
+ return null;
264
+ }
195
265
  function delay(ms) {
196
266
  return new Promise((resolve) => setTimeout(resolve, ms));
197
267
  }
198
268
  async function createPromptInterface() {
199
- return createInterface({
269
+ const rl = createInterface({
200
270
  input,
201
271
  output,
202
272
  terminal: true,
203
273
  });
274
+ rl.on("SIGINT", () => {
275
+ rl.close();
276
+ });
277
+ return rl;
278
+ }
279
+ async function question(rl, prompt) {
280
+ try {
281
+ return await rl.question(prompt);
282
+ }
283
+ catch (error) {
284
+ if (isReadlineClosedError(error)) {
285
+ throw createCliExitError();
286
+ }
287
+ throw error;
288
+ }
204
289
  }
205
290
  async function ask(rl, label, options) {
206
291
  const defaultValue = normalizeText(options?.defaultValue);
207
292
  const suffix = defaultValue ? ` ${style(`[${defaultValue}]`, ANSI.dim, supportsAnsi())}` : "";
208
- const answer = normalizeText(await rl.question(`${style("›", ANSI.coral, supportsAnsi())} ${label}${suffix}: `));
293
+ const answer = normalizeText(await question(rl, `${style("›", ANSI.brandBlue, supportsAnsi())} ${label}${suffix}: `));
209
294
  if (answer) {
210
295
  return answer;
211
296
  }
@@ -217,21 +302,21 @@ async function ask(rl, label, options) {
217
302
  }
218
303
  return await ask(rl, label, options);
219
304
  }
220
- async function askYesNo(rl, question, defaultValue = true) {
221
- const answer = normalizeText(await rl.question(`${style("?", ANSI.blue, supportsAnsi())} ${question} ${style(defaultValue ? "[Y/n]" : "[y/N]", ANSI.dim, supportsAnsi())}: `)).toLowerCase();
305
+ async function askYesNo(rl, promptText, defaultValue = true) {
306
+ const answer = normalizeText(await question(rl, `${style("?", ANSI.brandBlue, supportsAnsi())} ${promptText} ${style(defaultValue ? "[Y/n]" : "[y/N]", ANSI.dim, supportsAnsi())}: `)).toLowerCase();
222
307
  if (!answer) {
223
308
  return defaultValue;
224
309
  }
225
310
  return ["y", "yes", "s", "sim"].includes(answer);
226
311
  }
227
312
  async function pauseForEnter(rl, message = "Pressione Enter para continuar") {
228
- await rl.question(`${style("↵", ANSI.dim, supportsAnsi())} ${message}`);
313
+ await question(rl, `${style("↵", ANSI.dim, supportsAnsi())} ${message}`);
229
314
  }
230
315
  async function chooseExecutor(rl, current) {
231
316
  const defaultType = current?.type || resolveExecutorConfig().type;
232
317
  console.log([
233
- `${style("1.", ANSI.coral, supportsAnsi())} native-macos ${style("(Mac real, runtime local)", ANSI.dim, supportsAnsi())}`,
234
- `${style("2.", ANSI.coral, supportsAnsi())} mock ${style("(ambiente de teste)", ANSI.dim, supportsAnsi())}`,
318
+ `${style("1.", ANSI.brandBlue, supportsAnsi())} native-macos ${style("(Mac real, runtime local)", ANSI.dim, supportsAnsi())}`,
319
+ `${style("2.", ANSI.brandBlue, supportsAnsi())} mock ${style("(ambiente de teste)", ANSI.dim, supportsAnsi())}`,
235
320
  ].join("\n"));
236
321
  const selection = await ask(rl, "Executor", {
237
322
  defaultValue: defaultType === "mock" ? "2" : "1",
@@ -293,18 +378,212 @@ function extractConfirmationPrompt(job) {
293
378
  }
294
379
  function renderStatusOverview(config, runtimeSession) {
295
380
  return [
296
- `${style("Device", ANSI.blue, supportsAnsi())}: ${config.deviceName}`,
297
- `${style("Device ID", ANSI.blue, supportsAnsi())}: ${config.deviceId}`,
298
- `${style("API", ANSI.blue, supportsAnsi())}: ${config.apiBaseUrl}`,
299
- `${style("Executor", ANSI.blue, supportsAnsi())}: ${config.executor.type}`,
300
- `${style("Approval", ANSI.blue, supportsAnsi())}: ${config.approvalMode}`,
301
- `${style("Runtime", ANSI.blue, supportsAnsi())}: ${runtimeSession?.getStatusLabel() || "offline"}`,
381
+ `${style("Device", ANSI.brandBlue, supportsAnsi())}: ${config.deviceName}`,
382
+ `${style("Device ID", ANSI.brandBlue, supportsAnsi())}: ${config.deviceId}`,
383
+ `${style("API", ANSI.brandBlue, supportsAnsi())}: ${config.apiBaseUrl}`,
384
+ `${style("Executor", ANSI.brandBlue, supportsAnsi())}: ${config.executor.type}`,
385
+ `${style("Approval", ANSI.brandBlue, supportsAnsi())}: ${config.approvalMode}`,
386
+ `${style("Runtime", ANSI.brandBlue, supportsAnsi())}: ${runtimeSession?.getStatusLabel() || "offline"}`,
302
387
  ...(runtimeSession?.getStatusDetail()
303
- ? [`${style("Runtime note", ANSI.blue, supportsAnsi())}: ${runtimeSession.getStatusDetail()}`]
388
+ ? [`${style("Runtime note", ANSI.brandBlue, supportsAnsi())}: ${runtimeSession.getStatusDetail()}`]
304
389
  : []),
305
- `${style("Config", ANSI.blue, supportsAnsi())}: ${getBridgeConfigPath()}`,
390
+ `${style("Config", ANSI.brandBlue, supportsAnsi())}: ${getBridgeConfigPath()}`,
306
391
  ];
307
392
  }
393
+ function renderRuntimeHeadline(runtimeSession) {
394
+ if (!runtimeSession) {
395
+ return "bridge: aguardando pairing inicial";
396
+ }
397
+ const status = runtimeSession.getStatusLabel();
398
+ if (status === "online") {
399
+ return "bridge: pareado e pronto para uso local";
400
+ }
401
+ if (status === "starting") {
402
+ return "bridge: conectando o runtime local";
403
+ }
404
+ if (status === "reconnecting") {
405
+ return "bridge: reconectando com o backend";
406
+ }
407
+ if (status === "error") {
408
+ return "bridge: com problema de conexao, veja /status";
409
+ }
410
+ return "bridge: offline";
411
+ }
412
+ function padRight(text, width) {
413
+ return text.length >= width ? text : `${text}${" ".repeat(width - text.length)}`;
414
+ }
415
+ function styleCardLine(line, tone, enabled) {
416
+ if (!enabled) {
417
+ return line;
418
+ }
419
+ if (tone === "title") {
420
+ return `${ANSI.bold}${ANSI.brandBlue}${line}${ANSI.reset}`;
421
+ }
422
+ if (tone === "muted") {
423
+ return `${ANSI.slateItalic}${line}${ANSI.reset}`;
424
+ }
425
+ return `${ANSI.white}${line}${ANSI.reset}`;
426
+ }
427
+ function renderInfoCard(lines) {
428
+ const enabled = supportsAnsi();
429
+ const normalized = lines.map((line) => ({
430
+ text: truncate(line.text, 82),
431
+ tone: line.tone,
432
+ }));
433
+ const width = Math.max(44, ...normalized.map((line) => line.text.length));
434
+ const top = style(`┌${"─".repeat(width + 2)}┐`, ANSI.brandBlue, enabled);
435
+ const bottom = style(`└${"─".repeat(width + 2)}┘`, ANSI.brandBlue, enabled);
436
+ return [
437
+ top,
438
+ ...normalized.map((line) => {
439
+ const border = style("│", ANSI.brandBlue, enabled);
440
+ const content = styleCardLine(padRight(line.text, width), line.tone, enabled);
441
+ return `${border} ${content} ${border}`;
442
+ }),
443
+ bottom,
444
+ ].join("\n");
445
+ }
446
+ function buildWelcomeCard(runtimeSession, modelMode) {
447
+ return [
448
+ { text: "Welcome to OttoAI", tone: "title" },
449
+ { text: "", tone: "muted" },
450
+ { text: "/help inside the console, /status for bridge details", tone: "muted" },
451
+ { text: "", tone: "muted" },
452
+ { text: `model: ${getCliModelLabel(modelMode)}`, tone: "muted" },
453
+ { text: `cwd: ${process.cwd()}`, tone: "muted" },
454
+ { text: renderRuntimeHeadline(runtimeSession), tone: "muted" },
455
+ ];
456
+ }
457
+ function printHubScreen(runtimeSession, modelMode) {
458
+ console.clear();
459
+ console.log(renderBanner());
460
+ console.log("");
461
+ console.log(renderInfoCard(buildWelcomeCard(runtimeSession, modelMode)));
462
+ }
463
+ function printConsoleScreen(runtimeSession, modelMode) {
464
+ console.clear();
465
+ console.log(renderBanner());
466
+ console.log("");
467
+ console.log(renderInfoCard(buildWelcomeCard(runtimeSession, modelMode)));
468
+ console.log("");
469
+ console.log(style("Peça algo ao Otto", `${ANSI.bold}${ANSI.white}`, supportsAnsi()));
470
+ printSoft("Comandos: /help, /model [fast|thinking], /status, /clear, /exit");
471
+ console.log("");
472
+ }
473
+ function renderPromptFrameLine(width, edgeLeft, edgeRight) {
474
+ return style(`${edgeLeft}${"─".repeat(width)}${edgeRight}`, ANSI.brandBlue, supportsAnsi());
475
+ }
476
+ async function askConsoleInput(rl) {
477
+ const availableWidth = Number(output.columns || 90);
478
+ const frameWidth = Math.max(36, Math.min(availableWidth - 2, 88));
479
+ console.log(renderPromptFrameLine(frameWidth, "┌", "┐"));
480
+ const answer = normalizeText(await question(rl, `${style("│", ANSI.brandBlue, supportsAnsi())} ${style(">", `${ANSI.bold}${ANSI.white}`, supportsAnsi())} `));
481
+ console.log(renderPromptFrameLine(frameWidth, "└", "┘"));
482
+ return answer;
483
+ }
484
+ function printAssistantMessage(message) {
485
+ const text = normalizeText(message);
486
+ if (!text) {
487
+ return;
488
+ }
489
+ console.log(`${style("•", ANSI.brandBlue, supportsAnsi())} ${text}`);
490
+ }
491
+ function extractJobOutcome(job) {
492
+ const result = job.result && typeof job.result === "object"
493
+ ? job.result
494
+ : {};
495
+ return result.outcome && typeof result.outcome === "object"
496
+ ? result.outcome
497
+ : {};
498
+ }
499
+ function outcomePathLabel(outcome) {
500
+ return normalizeText(outcome.resolved_path || outcome.path);
501
+ }
502
+ function collectOutcomeFileContent(outcome) {
503
+ const directContent = outcome.content;
504
+ if (typeof directContent === "string" && directContent.trim()) {
505
+ return directContent;
506
+ }
507
+ const chunks = Array.isArray(outcome.content_chunks) ? outcome.content_chunks : [];
508
+ return chunks
509
+ .filter((item) => item && typeof item === "object")
510
+ .map((item) => String(item.text || ""))
511
+ .filter((item) => item.length > 0)
512
+ .join("");
513
+ }
514
+ function formatDirectoryEntry(entry) {
515
+ const name = normalizeText(entry.name) || "(sem nome)";
516
+ const kind = normalizeText(entry.kind).toLowerCase();
517
+ const prefix = kind === "directory" ? "[dir]" : "[file]";
518
+ const size = kind === "file" ? humanFileSize(entry.size_bytes) : "";
519
+ return `${prefix} ${name}${size ? ` (${size})` : ""}`;
520
+ }
521
+ export function renderStructuredOutcome(job, options) {
522
+ const compact = Boolean(options?.compact);
523
+ const outcome = extractJobOutcome(job);
524
+ const actionType = normalizeText(outcome.action_type).toLowerCase();
525
+ const lines = [];
526
+ if (actionType === "list_files") {
527
+ const entries = Array.isArray(outcome.entries)
528
+ ? outcome.entries.filter((item) => item && typeof item === "object")
529
+ : [];
530
+ const target = outcomePathLabel(outcome);
531
+ const itemCount = outcome.listed_item_count ?? entries.length;
532
+ if (target) {
533
+ lines.push(target);
534
+ }
535
+ if (itemCount) {
536
+ lines.push(`${itemCount} item(ns) encontrados`);
537
+ }
538
+ if (lines.length) {
539
+ lines.push("");
540
+ }
541
+ const maxEntries = compact ? MAX_RENDERED_LIST_ENTRIES_COMPACT : MAX_RENDERED_LIST_ENTRIES;
542
+ entries.slice(0, maxEntries).forEach((entry) => {
543
+ lines.push(formatDirectoryEntry(entry));
544
+ });
545
+ if (entries.length > maxEntries) {
546
+ lines.push(`... +${entries.length - maxEntries} item(ns)`);
547
+ }
548
+ }
549
+ if (actionType === "read_file") {
550
+ const target = outcomePathLabel(outcome);
551
+ const binaryNotice = normalizeText(outcome.binary_notice);
552
+ const content = collectOutcomeFileContent(outcome);
553
+ const maxChars = compact ? MAX_RENDERED_FILE_CHARS_COMPACT : MAX_RENDERED_FILE_CHARS;
554
+ if (target) {
555
+ lines.push(target);
556
+ lines.push("");
557
+ }
558
+ if (binaryNotice) {
559
+ lines.push(binaryNotice);
560
+ }
561
+ else if (content) {
562
+ const truncatedContent = content.length > maxChars
563
+ ? `${content.slice(0, maxChars).trimEnd()}\n\n[... conteúdo truncado ...]`
564
+ : content;
565
+ lines.push(truncatedContent);
566
+ }
567
+ }
568
+ return lines.join("\n").trim();
569
+ }
570
+ function printStructuredOutcome(job) {
571
+ const rendered = renderStructuredOutcome(job);
572
+ if (!rendered) {
573
+ return;
574
+ }
575
+ console.log(rendered);
576
+ }
577
+ function buildConversationSummary(summary, job) {
578
+ const rendered = renderStructuredOutcome(job, { compact: true });
579
+ if (!rendered) {
580
+ return summary;
581
+ }
582
+ if (!summary) {
583
+ return rendered;
584
+ }
585
+ return `${summary}\n${rendered}`.slice(0, 4_000).trim();
586
+ }
308
587
  async function printExtensionsOverview(config) {
309
588
  printSection("Extensions");
310
589
  if (!config.installedExtensions.length) {
@@ -371,7 +650,7 @@ async function followConsoleJob(rl, config, jobId) {
371
650
  const stepId = extractJobStepId(job);
372
651
  if (status !== lastStatus || stepId !== lastStepId) {
373
652
  const statusLabel = `${status}${stepId ? ` · ${stepId}` : ""}`;
374
- console.log(`${style("runtime", ANSI.teal, supportsAnsi())} ${statusLabel}`);
653
+ printMuted(`local: ${statusLabel}`);
375
654
  lastStatus = status;
376
655
  lastStepId = stepId;
377
656
  }
@@ -401,20 +680,20 @@ async function followConsoleJob(rl, config, jobId) {
401
680
  : status === "failed"
402
681
  ? "Execução local falhou."
403
682
  : "Execução local cancelada.");
404
- console.log(`${style("otto", ANSI.coral, supportsAnsi())} ${summary}`);
405
- return summary;
683
+ printAssistantMessage(summary);
684
+ printStructuredOutcome(job);
685
+ return buildConversationSummary(summary, job);
406
686
  }
407
687
  await delay(1400);
408
688
  }
409
689
  }
410
690
  async function runOttoConsole(rl, config, runtimeSession, options) {
411
- printSection("Otto Console");
412
- printMuted("Este console usa o mesmo runtime local já ligado pelo `otto-bridge`.");
413
- printMuted(`Runtime: ${runtimeSession.getStatusLabel()} · ${runtimeSession.getStatusDetail()}`);
691
+ let activeModel = "fast";
692
+ printConsoleScreen(runtimeSession, activeModel);
414
693
  const sessionId = randomUUID();
415
694
  const conversation = [];
416
695
  const printConsoleHelp = () => {
417
- printMuted("Comandos: /help, /clear, /status, /exit");
696
+ printSoft("Comandos: /help, /model [fast|thinking], /status, /clear, /exit");
418
697
  };
419
698
  const handlePrompt = async (promptText) => {
420
699
  const normalizedPrompt = normalizeText(promptText);
@@ -427,10 +706,32 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
427
706
  }
428
707
  if (normalizedPrompt === "/clear") {
429
708
  conversation.splice(0, conversation.length);
709
+ printConsoleScreen(runtimeSession, activeModel);
430
710
  printMuted("Contexto local do console limpo.");
431
711
  return;
432
712
  }
713
+ if (normalizedPrompt === "/model") {
714
+ printMuted(`Modelo ativo: ${getCliModelLabel(activeModel)}.`);
715
+ printSoft("Use /model fast ou /model thinking para trocar.");
716
+ return;
717
+ }
718
+ if (normalizedPrompt.startsWith("/model ")) {
719
+ const nextMode = resolveCliModelMode(normalizedPrompt.slice("/model ".length));
720
+ if (!nextMode) {
721
+ printWarning("Modelo inválido. Use /model fast ou /model thinking.");
722
+ return;
723
+ }
724
+ if (nextMode === activeModel) {
725
+ printMuted(`Modelo já está em ${getCliModelLabel(activeModel)}.`);
726
+ return;
727
+ }
728
+ activeModel = nextMode;
729
+ printConsoleScreen(runtimeSession, activeModel);
730
+ printSuccess(`Modelo ativo: ${getCliModelLabel(activeModel)}.`);
731
+ return;
732
+ }
433
733
  if (normalizedPrompt === "/status") {
734
+ console.log(`${style("Model", ANSI.brandBlue, supportsAnsi())}: ${getCliModelLabel(activeModel)}`);
434
735
  renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
435
736
  const runtimeFailure = runtimeSession.getLastError();
436
737
  if (runtimeFailure) {
@@ -439,18 +740,18 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
439
740
  return;
440
741
  }
441
742
  if (normalizedPrompt === "/exit") {
442
- throw new Error("__OTTO_CONSOLE_EXIT__");
743
+ throw createCliExitError();
443
744
  }
444
745
  conversation.push({ role: "user", content: normalizedPrompt });
445
746
  while (conversation.length > 18) {
446
747
  conversation.shift();
447
748
  }
448
- console.log(`${style("você", ANSI.white, supportsAnsi())} ${normalizedPrompt}`);
449
749
  let streamedAssistant = "";
450
750
  let assistantPrefixPrinted = false;
451
751
  let handoffPayload = null;
452
752
  await streamDeviceCliChat(config, {
453
753
  messages: conversation,
754
+ model: getCliModelRequestModel(activeModel),
454
755
  session_id: sessionId,
455
756
  }, async (event) => {
456
757
  const chunkType = normalizeText(event.chunk_type).toLowerCase();
@@ -475,7 +776,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
475
776
  return;
476
777
  }
477
778
  if (!assistantPrefixPrinted) {
478
- output.write(`${style("otto", ANSI.coral, supportsAnsi())} `);
779
+ output.write(`${style("", ANSI.brandBlue, supportsAnsi())} `);
479
780
  assistantPrefixPrinted = true;
480
781
  }
481
782
  output.write(contentChunk);
@@ -489,7 +790,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
489
790
  const handoffData = handoffPayload;
490
791
  const bridgeSummary = extractBridgeHandoffSummary(handoffData);
491
792
  if (bridgeSummary) {
492
- console.log(`${style("otto", ANSI.coral, supportsAnsi())} ${bridgeSummary}`);
793
+ printAssistantMessage(bridgeSummary);
493
794
  }
494
795
  const job = handoffData.job && typeof handoffData.job === "object"
495
796
  ? handoffData.job
@@ -514,12 +815,12 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
514
815
  await handlePrompt(options.initialPrompt);
515
816
  }
516
817
  for (;;) {
517
- const promptText = await ask(rl, "OTTO", { allowEmpty: true });
818
+ const promptText = await askConsoleInput(rl);
518
819
  try {
519
820
  await handlePrompt(promptText);
520
821
  }
521
822
  catch (error) {
522
- if (error instanceof Error && error.message === "__OTTO_CONSOLE_EXIT__") {
823
+ if (isCliExitError(error)) {
523
824
  break;
524
825
  }
525
826
  throw error;
@@ -536,15 +837,15 @@ async function pickHomeChoice(rl, paired) {
536
837
  printSection("Home");
537
838
  const options = paired
538
839
  ? [
539
- `${style("1.", ANSI.coral, supportsAnsi())} Otto Console`,
540
- `${style("2.", ANSI.coral, supportsAnsi())} Re-pair / setup`,
541
- `${style("3.", ANSI.coral, supportsAnsi())} Status do bridge`,
542
- `${style("4.", ANSI.coral, supportsAnsi())} Extensões instaladas`,
543
- `${style("5.", ANSI.coral, supportsAnsi())} Sair`,
840
+ `${style("1.", ANSI.brandBlue, supportsAnsi())} Otto Console`,
841
+ `${style("2.", ANSI.brandBlue, supportsAnsi())} Setup / parear novamente`,
842
+ `${style("3.", ANSI.brandBlue, supportsAnsi())} Status detalhado`,
843
+ `${style("4.", ANSI.brandBlue, supportsAnsi())} Extensões instaladas`,
844
+ `${style("5.", ANSI.brandBlue, supportsAnsi())} Sair`,
544
845
  ]
545
846
  : [
546
- `${style("1.", ANSI.coral, supportsAnsi())} Pairing setup`,
547
- `${style("2.", ANSI.coral, supportsAnsi())} Sair`,
847
+ `${style("1.", ANSI.brandBlue, supportsAnsi())} Pairing setup`,
848
+ `${style("2.", ANSI.brandBlue, supportsAnsi())} Sair`,
548
849
  ];
549
850
  console.log(options.join("\n"));
550
851
  const answer = await ask(rl, "Escolha");
@@ -565,10 +866,9 @@ export async function launchInteractiveCli(options) {
565
866
  const rl = await createPromptInterface();
566
867
  let runtimeSession = null;
567
868
  try {
568
- console.clear();
569
- console.log(renderBanner());
570
869
  let config = await loadBridgeConfig();
571
870
  if (!config) {
871
+ printHubScreen(null, "fast");
572
872
  const setup = await runSetupWizard(rl, options);
573
873
  config = setup.config;
574
874
  if (config && setup.openConsole) {
@@ -583,8 +883,7 @@ export async function launchInteractiveCli(options) {
583
883
  runtimeSession = runtimeSession || new CliRuntimeSession(config);
584
884
  await runtimeSession.ensureStarted();
585
885
  for (;;) {
586
- console.log("");
587
- renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
886
+ printHubScreen(runtimeSession, "fast");
588
887
  const choice = await pickHomeChoice(rl, true);
589
888
  if (choice === "exit") {
590
889
  break;
@@ -620,6 +919,11 @@ export async function launchInteractiveCli(options) {
620
919
  }
621
920
  }
622
921
  }
922
+ catch (error) {
923
+ if (!isCliExitError(error)) {
924
+ throw error;
925
+ }
926
+ }
623
927
  finally {
624
928
  await runtimeSession?.stop().catch(() => undefined);
625
929
  rl.close();
@@ -629,8 +933,7 @@ export async function runSetupCommand(options) {
629
933
  const rl = await createPromptInterface();
630
934
  let runtimeSession = null;
631
935
  try {
632
- console.clear();
633
- console.log(renderBanner());
936
+ printHubScreen(null, "fast");
634
937
  const setup = await runSetupWizard(rl, options);
635
938
  if (setup.config && setup.openConsole) {
636
939
  runtimeSession = new CliRuntimeSession(setup.config);
@@ -638,6 +941,11 @@ export async function runSetupCommand(options) {
638
941
  await runOttoConsole(rl, setup.config, runtimeSession);
639
942
  }
640
943
  }
944
+ catch (error) {
945
+ if (!isCliExitError(error)) {
946
+ throw error;
947
+ }
948
+ }
641
949
  finally {
642
950
  await runtimeSession?.stop().catch(() => undefined);
643
951
  rl.close();
@@ -647,8 +955,7 @@ export async function runConsoleCommand(initialPrompt) {
647
955
  const rl = await createPromptInterface();
648
956
  let runtimeSession = null;
649
957
  try {
650
- console.clear();
651
- console.log(renderBanner());
958
+ printHubScreen(null, "fast");
652
959
  let config = await loadBridgeConfig();
653
960
  if (!config) {
654
961
  const setup = await runSetupWizard(rl);
@@ -661,6 +968,11 @@ export async function runConsoleCommand(initialPrompt) {
661
968
  await runtimeSession.ensureStarted();
662
969
  await runOttoConsole(rl, config, runtimeSession, { initialPrompt });
663
970
  }
971
+ catch (error) {
972
+ if (!isCliExitError(error)) {
973
+ throw error;
974
+ }
975
+ }
664
976
  finally {
665
977
  await runtimeSession?.stop().catch(() => undefined);
666
978
  rl.close();
@@ -4563,7 +4563,7 @@ return {
4563
4563
  qrStabilityWindowMs: WHATSAPP_EXPECTED_CONNECTED_QR_STABILITY_WINDOW_MS,
4564
4564
  });
4565
4565
  if (state.connected) {
4566
- await this.syncWhatsAppExtensionState("connected", "Sessao local do WhatsApp Web mantida em background enquanto `otto-bridge run` estiver ativo.", { runtimeAttached: true });
4566
+ await this.syncWhatsAppExtensionState("connected", "Sessao local do WhatsApp Web mantida em background enquanto `otto-bridge` estiver ativo.", { runtimeAttached: true });
4567
4567
  return;
4568
4568
  }
4569
4569
  await this.syncWhatsAppExtensionState("session_expired", state.qrVisible
package/dist/main.js CHANGED
@@ -4,7 +4,6 @@ import process from "node:process";
4
4
  import { clearBridgeConfig, getBridgeConfigPath, loadBridgeConfig, normalizeInstalledExtensions, resolveApiBaseUrl, resolveExecutorConfig, saveBridgeConfig, } from "./config.js";
5
5
  import { buildInstalledManagedExtensionState, formatManagedBridgeExtensionStatus, getManagedBridgeExtensionDefinition, isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, removeManagedBridgeExtensionState, saveManagedBridgeExtensionState, } from "./extensions.js";
6
6
  import { pairDevice } from "./pairing.js";
7
- import { BridgeRuntime } from "./runtime.js";
8
7
  import { detectWhatsAppBackgroundStatus, runWhatsAppBackgroundSetup, } from "./whatsapp_background.js";
9
8
  import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
10
9
  import { launchInteractiveCli, runConsoleCommand, runSetupCommand, } from "./cli_terminal.js";
@@ -13,8 +12,8 @@ const UPDATE_RETRY_DELAYS_MS = [0, 8_000, 20_000];
13
12
  function parseArgs(argv) {
14
13
  const [maybeCommand, ...rest] = argv;
15
14
  if (!maybeCommand) {
16
- const interactiveDefault = process.stdout.isTTY && process.stdin.isTTY && process.env.OTTO_BRIDGE_LEGACY_DEFAULT_RUN !== "1";
17
- return { command: interactiveDefault ? "home" : "run", options: new Map() };
15
+ const interactiveDefault = process.stdout.isTTY && process.stdin.isTTY;
16
+ return { command: interactiveDefault ? "home" : "help", options: new Map() };
18
17
  }
19
18
  if (maybeCommand === "--help" || maybeCommand === "-h") {
20
19
  return { command: "help", options: new Map() };
@@ -106,16 +105,14 @@ function resolveExecutorOverrides(args, current) {
106
105
  function printUsage() {
107
106
  console.log(`Usage:
108
107
  otto-bridge
109
- otto-bridge home
110
108
  otto-bridge setup
111
109
  otto-bridge console
112
- otto-bridge pair --api http://localhost:8000 --code ABC123 [--name "Meu PC"] [--executor native-macos|mock|clawd-cursor]
113
110
  otto-bridge status
114
111
  otto-bridge extensions --list
115
- otto-bridge extensions --install github
116
- otto-bridge extensions --setup whatsappweb
117
- otto-bridge extensions --status whatsappweb
118
- otto-bridge extensions --uninstall github
112
+ otto-bridge extensions --install <name>
113
+ otto-bridge extensions --setup <name>
114
+ otto-bridge extensions --status <name>
115
+ otto-bridge extensions --uninstall <name>
119
116
  otto-bridge version
120
117
  otto-bridge update [--tag latest|next] [--dry-run]
121
118
  otto-bridge unpair
@@ -124,13 +121,11 @@ Examples:
124
121
  otto-bridge
125
122
  otto-bridge setup
126
123
  otto-bridge console
127
- otto-bridge pair --api https://api.leg3ndy.com.br --code ABC123
128
- otto-bridge extensions --install whatsappweb
129
- otto-bridge extensions --setup whatsappweb
130
- otto-bridge extensions --status whatsappweb
124
+ otto-bridge extensions --install <name>
125
+ otto-bridge extensions --setup <name>
126
+ otto-bridge extensions --status <name>
131
127
  otto-bridge extensions --list
132
128
  otto-bridge version
133
- otto-bridge update
134
129
  otto-bridge update --dry-run
135
130
  otto-bridge --version`);
136
131
  }
@@ -249,7 +244,7 @@ async function detectManagedExtensionStatus(slug, currentState) {
249
244
  if (hasFreshRuntimeAttachment(currentState)) {
250
245
  return {
251
246
  status: "connected",
252
- notes: currentState?.notes || "Sessao local do WhatsApp Web mantida em background enquanto `otto-bridge run` estiver ativo.",
247
+ notes: currentState?.notes || "Sessao local do WhatsApp Web mantida em background enquanto `otto-bridge` estiver ativo.",
253
248
  };
254
249
  }
255
250
  const detected = await detectManagedWhatsAppWebStatus();
@@ -290,44 +285,10 @@ async function runPairCommand(args) {
290
285
  async function loadRequiredBridgeConfig() {
291
286
  const config = await loadBridgeConfig();
292
287
  if (!config) {
293
- throw new Error("No local pairing found. Run `otto-bridge pair --code <CODE>` first.");
288
+ throw new Error("Nenhum pairing local encontrado. Rode `otto-bridge` ou `otto-bridge setup` primeiro.");
294
289
  }
295
290
  return config;
296
291
  }
297
- async function runRuntimeCommand(args) {
298
- console.log("[otto-bridge] `run` agora é um alias legado. Prefira `otto-bridge`.");
299
- const config = await loadRequiredBridgeConfig();
300
- const runtimeConfig = {
301
- ...config,
302
- executor: resolveExecutorOverrides(args, config.executor),
303
- };
304
- const runtime = new BridgeRuntime(runtimeConfig);
305
- let stopping = false;
306
- const shutdown = async (signal) => {
307
- if (stopping) {
308
- return;
309
- }
310
- stopping = true;
311
- console.log(`[otto-bridge] shutting down runtime after ${signal}`);
312
- await runtime.stop().catch(() => undefined);
313
- };
314
- const handleSigint = () => {
315
- void shutdown("SIGINT");
316
- };
317
- const handleSigterm = () => {
318
- void shutdown("SIGTERM");
319
- };
320
- process.once("SIGINT", handleSigint);
321
- process.once("SIGTERM", handleSigterm);
322
- try {
323
- await runtime.start();
324
- }
325
- finally {
326
- process.off("SIGINT", handleSigint);
327
- process.off("SIGTERM", handleSigterm);
328
- await runtime.stop().catch(() => undefined);
329
- }
330
- }
331
292
  async function runStatusCommand() {
332
293
  const config = await loadBridgeConfig();
333
294
  if (!config) {
@@ -395,7 +356,7 @@ async function runExtensionsCommand(args) {
395
356
  }
396
357
  console.log(`[otto-bridge] proximo passo para ${extension}: otto-bridge extensions --setup ${extension}`);
397
358
  }
398
- console.log("[otto-bridge] rode `otto-bridge run` novamente se quiser sincronizar agora com a web");
359
+ console.log("[otto-bridge] reabra `otto-bridge` se quiser sincronizar agora com a web");
399
360
  return;
400
361
  }
401
362
  if (setupValue) {
@@ -488,7 +449,7 @@ async function runExtensionsCommand(args) {
488
449
  }
489
450
  }
490
451
  console.log(`[otto-bridge] extensoes removidas: ${removed.join(", ")}`);
491
- console.log("[otto-bridge] rode `otto-bridge run` novamente se quiser sincronizar agora com a web");
452
+ console.log("[otto-bridge] reabra `otto-bridge` se quiser sincronizar agora com a web");
492
453
  return;
493
454
  }
494
455
  if (!config.installedExtensions.length) {
@@ -600,7 +561,7 @@ async function main() {
600
561
  await runPairCommand(args);
601
562
  return;
602
563
  case "run":
603
- await runRuntimeCommand(args);
564
+ await launchInteractiveCli();
604
565
  return;
605
566
  case "status":
606
567
  await runStatusCommand();
package/dist/runtime.js CHANGED
@@ -84,6 +84,8 @@ export class BridgeRuntime {
84
84
  localAutomationRuntime;
85
85
  lastBridgeReleaseNoticeKey = null;
86
86
  activeSocket = null;
87
+ reconnectTimer = null;
88
+ reconnectWaitResolver = null;
87
89
  stopped = false;
88
90
  started = false;
89
91
  pendingConfirmations = new Map();
@@ -206,12 +208,21 @@ export class BridgeRuntime {
206
208
  }
207
209
  this.logInfo(`[otto-bridge] reconnecting in ${this.reconnectDelayMs}ms`);
208
210
  this.emit({ type: "reconnecting", delayMs: this.reconnectDelayMs });
209
- await delay(this.reconnectDelayMs);
211
+ await this.waitForReconnect(this.reconnectDelayMs);
210
212
  this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, DEFAULT_RECONNECT_MAX_DELAY_MS);
211
213
  }
212
214
  }
213
215
  async stop() {
214
216
  this.stopped = true;
217
+ if (this.reconnectTimer) {
218
+ clearTimeout(this.reconnectTimer);
219
+ this.reconnectTimer = null;
220
+ }
221
+ if (this.reconnectWaitResolver) {
222
+ const resolve = this.reconnectWaitResolver;
223
+ this.reconnectWaitResolver = null;
224
+ resolve();
225
+ }
215
226
  for (const [jobId, cancel] of this.activeCancels.entries()) {
216
227
  try {
217
228
  await cancel();
@@ -237,6 +248,27 @@ export class BridgeRuntime {
237
248
  await this.executor.close();
238
249
  }
239
250
  }
251
+ async waitForReconnect(ms) {
252
+ if (ms <= 0 || this.stopped) {
253
+ return;
254
+ }
255
+ await new Promise((resolve) => {
256
+ this.reconnectWaitResolver = () => {
257
+ if (this.reconnectTimer) {
258
+ clearTimeout(this.reconnectTimer);
259
+ this.reconnectTimer = null;
260
+ }
261
+ this.reconnectWaitResolver = null;
262
+ resolve();
263
+ };
264
+ this.reconnectTimer = setTimeout(() => {
265
+ const finish = this.reconnectWaitResolver;
266
+ this.reconnectTimer = null;
267
+ this.reconnectWaitResolver = null;
268
+ finish?.();
269
+ }, ms);
270
+ });
271
+ }
240
272
  async connectOnce() {
241
273
  const socket = new WebSocket(this.config.wsUrl, ["device", this.config.deviceToken]);
242
274
  this.activeSocket = socket;
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "1.0.1";
2
+ export const BRIDGE_VERSION = "1.0.3";
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": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",
@@ -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.1");
27
+ console.log("\n[otto-bridge] Welcome to OTTOAI 1.0.3");
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"], {