@leg3ndy/otto-bridge 1.0.7 → 1.0.9

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/releases/OTTO_BRIDGE_0_9_0_RELEASE.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_0_9_0_RELEASE.md).
17
17
 
18
- Para a release atual `1.0.7`, com hotfix do input-box TTY do console, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_0_7_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_0_7_PATCH.md).
18
+ Para a release atual `1.0.9`, com composer multiline no console TTY e quebra de linha dentro da moldura, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_0_9_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_0_9_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.7.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.0.9.tgz
42
42
  ```
43
43
 
44
- Na linha `1.0.7`, `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.9`, `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.7` 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.9` 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.7` preserva esse fluxo e corrige o freeze do input-box TTY introduzido no refinamento visual do console.
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.9` preserva esse fluxo, mantem o input-box TTY estavel, adiciona composer multiline e melhora as views de status/extensoes e o refresh ao sair do `Terminal`.
49
49
 
50
50
  ## Publicacao
51
51
 
@@ -157,7 +157,7 @@ Esse comando abre um shell local interativo para instalar extensoes, rodar coman
157
157
 
158
158
  ### WhatsApp Web em background
159
159
 
160
- Fluxo recomendado na linha `1.0.7`:
160
+ Fluxo recomendado na linha `1.0.9`:
161
161
 
162
162
  ```bash
163
163
  otto-bridge extensions --install whatsappweb
@@ -167,13 +167,13 @@ otto-bridge extensions --status whatsappweb
167
167
 
168
168
  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.
169
169
 
170
- Contrato da linha `1.0.7`:
170
+ Contrato da linha `1.0.9`:
171
171
 
172
172
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
173
173
  - `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
174
174
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
175
175
 
176
- ## Handoff rapido da linha 1.0.7
176
+ ## Handoff rapido da linha 1.0.9
177
177
 
178
178
  Ja fechado no codigo:
179
179
 
@@ -1,9 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
+ import { readFile } from "node:fs/promises";
3
4
  import { createInterface } from "node:readline/promises";
4
5
  import { cursorTo, moveCursor, } from "node:readline";
5
6
  import process, { stdin as input, stdout as output } from "node:process";
6
- import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
7
+ import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, normalizeInstalledExtensions, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
7
8
  import { streamDeviceCliChat, } from "./chat_cli_client.js";
8
9
  import { formatManagedBridgeExtensionStatus, isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
9
10
  import { pairDevice } from "./pairing.js";
@@ -308,6 +309,47 @@ export function resolveCliModelMode(value) {
308
309
  function delay(ms) {
309
310
  return new Promise((resolve) => setTimeout(resolve, ms));
310
311
  }
312
+ async function resolveInstalledBridgeVersion() {
313
+ try {
314
+ const raw = await readFile(new URL("../package.json", import.meta.url), "utf8");
315
+ const parsed = JSON.parse(raw);
316
+ return normalizeText(parsed.version) || BRIDGE_VERSION;
317
+ }
318
+ catch {
319
+ return BRIDGE_VERSION;
320
+ }
321
+ }
322
+ async function captureBridgeRuntimeSnapshot(config) {
323
+ const managedExtensionState = await Promise.all(normalizeInstalledExtensions(config?.installedExtensions || [])
324
+ .filter(isManagedBridgeExtensionSlug)
325
+ .sort((left, right) => left.localeCompare(right))
326
+ .map(async (slug) => {
327
+ const state = await loadManagedBridgeExtensionState(slug);
328
+ return {
329
+ slug,
330
+ status: normalizeText(state?.status),
331
+ lastSetupAt: normalizeText(state?.lastSetupAt),
332
+ notes: normalizeText(state?.notes),
333
+ };
334
+ }));
335
+ return {
336
+ bridgeVersion: await resolveInstalledBridgeVersion(),
337
+ configFingerprint: JSON.stringify(config || null),
338
+ extensionStateFingerprint: JSON.stringify(managedExtensionState),
339
+ };
340
+ }
341
+ export function diffBridgeRuntimeSnapshots(before, after) {
342
+ const versionChanged = before.bridgeVersion !== after.bridgeVersion;
343
+ const configChanged = before.configFingerprint !== after.configFingerprint;
344
+ const extensionStateChanged = before.extensionStateFingerprint !== after.extensionStateFingerprint;
345
+ return {
346
+ versionChanged,
347
+ configChanged,
348
+ extensionStateChanged,
349
+ requiresCliRestart: versionChanged,
350
+ requiresRuntimeRefresh: configChanged || extensionStateChanged,
351
+ };
352
+ }
311
353
  async function createPromptInterface() {
312
354
  const rl = createInterface({
313
355
  input,
@@ -539,6 +581,72 @@ function buildBridgeReleaseCard(notice) {
539
581
  { text: `Rode: ${notice.updateCommand || "otto-bridge update"}`, tone: "primary" },
540
582
  ];
541
583
  }
584
+ function buildStatusCard(config, runtimeSession) {
585
+ const releaseNotice = runtimeSession.getReleaseNotice();
586
+ return [
587
+ { text: "Bridge Status", tone: "title" },
588
+ { text: "", tone: "muted" },
589
+ { text: `device: ${config.deviceName}`, tone: "primary" },
590
+ { text: `device id: ${config.deviceId}`, tone: "primary" },
591
+ { text: `api: ${config.apiBaseUrl}`, tone: "primary" },
592
+ { text: `executor: ${config.executor.type}`, tone: "primary" },
593
+ { text: `approval: ${config.approvalMode}`, tone: "primary" },
594
+ { text: `runtime: ${runtimeSession.getStatusLabel()}`, tone: "primary" },
595
+ ...(runtimeSession.getStatusDetail()
596
+ ? [{ text: `runtime note: ${runtimeSession.getStatusDetail()}`, tone: "muted" }]
597
+ : []),
598
+ ...(releaseNotice
599
+ ? [
600
+ { text: "", tone: "muted" },
601
+ {
602
+ text: `bridge update: ${releaseNotice.kind === "required" ? "required" : "available"}`,
603
+ tone: releaseNotice.kind === "required" ? "warning" : "primary",
604
+ },
605
+ ...(releaseNotice.currentVersion
606
+ ? [{ text: `current version: ${releaseNotice.currentVersion}`, tone: "primary" }]
607
+ : []),
608
+ ...(releaseNotice.latestVersion
609
+ ? [{ text: `latest version: ${releaseNotice.latestVersion}`, tone: "primary" }]
610
+ : []),
611
+ ...(releaseNotice.minSupportedVersion
612
+ ? [{ text: `min supported: ${releaseNotice.minSupportedVersion}`, tone: "primary" }]
613
+ : []),
614
+ { text: `update command: ${releaseNotice.updateCommand || "otto-bridge update"}`, tone: "muted" },
615
+ ]
616
+ : []),
617
+ { text: "", tone: "muted" },
618
+ { text: `config: ${getBridgeConfigPath()}`, tone: "muted" },
619
+ ];
620
+ }
621
+ async function buildExtensionsCard(config) {
622
+ const lines = [
623
+ { text: "Extensões", tone: "title" },
624
+ { text: "", tone: "muted" },
625
+ ];
626
+ if (!config.installedExtensions.length) {
627
+ lines.push({ text: "Nenhuma extensão instalada neste bridge.", tone: "muted" });
628
+ return lines;
629
+ }
630
+ for (const extension of config.installedExtensions) {
631
+ if (!isManagedBridgeExtensionSlug(extension)) {
632
+ lines.push({ text: `• ${extension}`, tone: "primary" });
633
+ lines.push({ text: "Extensão local sem status gerenciado.", tone: "muted" });
634
+ lines.push({ text: "", tone: "muted" });
635
+ continue;
636
+ }
637
+ const state = await loadManagedBridgeExtensionState(extension);
638
+ const status = state ? formatManagedBridgeExtensionStatus(state.status) : "sem estado salvo";
639
+ lines.push({ text: `• ${extension}: ${status}`, tone: "primary" });
640
+ if (state?.notes) {
641
+ lines.push({ text: truncate(state.notes, 160), tone: "muted" });
642
+ }
643
+ lines.push({ text: "", tone: "muted" });
644
+ }
645
+ while (lines.length > 2 && !normalizeText(lines[lines.length - 1]?.text)) {
646
+ lines.pop();
647
+ }
648
+ return lines;
649
+ }
542
650
  function printHubScreen(runtimeSession, modelMode) {
543
651
  clearScreen();
544
652
  console.log(renderBanner());
@@ -561,6 +669,8 @@ function printConsoleScreen(runtimeSession, modelMode) {
561
669
  console.log(renderInfoCard(buildBridgeReleaseCard(releaseNotice)));
562
670
  }
563
671
  console.log("");
672
+ printSoft(`Comandos: ${CONSOLE_COMMAND_HINT}`);
673
+ console.log("");
564
674
  }
565
675
  function renderPromptFrameLine(width, edgeLeft, edgeRight) {
566
676
  return style(`${edgeLeft}${"─".repeat(width)}${edgeRight}`, ANSI.brandBlue, supportsAnsi());
@@ -578,10 +688,130 @@ function renderConsolePromptContentLine(text, width, tone) {
578
688
  : style(clipped, ANSI.slateItalic, enabled);
579
689
  return `${border} ${body} ${border}`;
580
690
  }
691
+ function sliceByWidth(text, width) {
692
+ if (width <= 0) {
693
+ return [""];
694
+ }
695
+ if (!text.length) {
696
+ return [""];
697
+ }
698
+ const parts = [];
699
+ let offset = 0;
700
+ while (offset < text.length) {
701
+ parts.push(text.slice(offset, offset + width));
702
+ offset += width;
703
+ }
704
+ return parts.length ? parts : [""];
705
+ }
706
+ export function buildConsoleComposerLayout(value, innerWidth) {
707
+ const promptWidth = 2;
708
+ const lineContentWidth = Math.max(1, innerWidth - promptWidth);
709
+ if (!value) {
710
+ return {
711
+ lines: [CONSOLE_PLACEHOLDER],
712
+ cursorLineIndex: 0,
713
+ cursorColumn: 0,
714
+ };
715
+ }
716
+ const logicalLines = value.split("\n");
717
+ const lastLogicalLine = logicalLines[logicalLines.length - 1] ?? "";
718
+ const lines = [];
719
+ for (const logicalLine of logicalLines) {
720
+ const wrapped = sliceByWidth(logicalLine, lineContentWidth);
721
+ lines.push(...wrapped);
722
+ }
723
+ const trailingNewline = value.endsWith("\n");
724
+ const needsSoftWrapCursorRow = !trailingNewline
725
+ && lastLogicalLine.length > 0
726
+ && lastLogicalLine.length % lineContentWidth === 0;
727
+ if (trailingNewline || needsSoftWrapCursorRow) {
728
+ lines.push("");
729
+ }
730
+ const cursorLineIndex = trailingNewline
731
+ ? lines.length - 1
732
+ : needsSoftWrapCursorRow
733
+ ? lines.length - 1
734
+ : Math.max(0, lines.length - 1);
735
+ const cursorColumn = trailingNewline
736
+ ? 0
737
+ : needsSoftWrapCursorRow
738
+ ? 0
739
+ : lastLogicalLine.length % lineContentWidth;
740
+ return {
741
+ lines,
742
+ cursorLineIndex,
743
+ cursorColumn,
744
+ };
745
+ }
746
+ function renderConsoleComposerLines(value, innerWidth, enabled) {
747
+ const layout = buildConsoleComposerLayout(value, innerWidth);
748
+ const lineContentWidth = Math.max(1, innerWidth - 2);
749
+ const promptStyled = style(">", `${ANSI.bold}${ANSI.white}`, enabled);
750
+ if (!value) {
751
+ const placeholder = truncate(CONSOLE_PLACEHOLDER, lineContentWidth);
752
+ return {
753
+ renderedLines: [
754
+ `${promptStyled} ${style(placeholder, ANSI.slate, enabled)}${" ".repeat(Math.max(0, lineContentWidth - placeholder.length))}`,
755
+ ],
756
+ cursorLineIndex: 0,
757
+ cursorColumn: 0,
758
+ };
759
+ }
760
+ return {
761
+ renderedLines: layout.lines.map((line, index) => {
762
+ const prefix = index === 0 ? `${promptStyled} ` : " ";
763
+ return `${prefix}${line}${" ".repeat(Math.max(0, lineContentWidth - line.length))}`;
764
+ }),
765
+ cursorLineIndex: layout.cursorLineIndex,
766
+ cursorColumn: layout.cursorColumn,
767
+ };
768
+ }
769
+ function tryConsumeControlSequence(buffer) {
770
+ const knownNewlineSequences = [
771
+ "\u001b[13;2u",
772
+ "\u001b[27;2;13~",
773
+ "\u001b[1;2M",
774
+ "\u001b\r",
775
+ "\u001b\n",
776
+ ];
777
+ for (const sequence of knownNewlineSequences) {
778
+ if (buffer.startsWith(sequence)) {
779
+ return {
780
+ consumed: sequence.length,
781
+ action: "newline",
782
+ };
783
+ }
784
+ }
785
+ if (buffer === "\u001b" || /^\u001b\[[0-9;?]*$/.test(buffer)) {
786
+ return {
787
+ consumed: 0,
788
+ action: "incomplete",
789
+ };
790
+ }
791
+ const csiMatch = buffer.match(/^\u001b\[[0-9;?]*[ -/]*[@-~]/);
792
+ if (csiMatch) {
793
+ return {
794
+ consumed: csiMatch[0].length,
795
+ action: "ignore",
796
+ };
797
+ }
798
+ const ss3Match = buffer.match(/^\u001bO./);
799
+ if (ss3Match) {
800
+ return {
801
+ consumed: ss3Match[0].length,
802
+ action: "ignore",
803
+ };
804
+ }
805
+ if (buffer.startsWith("\u001b")) {
806
+ return {
807
+ consumed: 1,
808
+ action: "ignore",
809
+ };
810
+ }
811
+ return null;
812
+ }
581
813
  async function askConsoleInput(rl) {
582
814
  if (!supportsAnsi() || typeof input.setRawMode !== "function" || !input.isTTY) {
583
- printSoft(`Comandos: ${CONSOLE_COMMAND_HINT}`);
584
- console.log("");
585
815
  return normalizeText(await question(rl, "> "));
586
816
  }
587
817
  rl.pause();
@@ -591,71 +821,90 @@ async function askConsoleInput(rl) {
591
821
  const enabled = supportsAnsi();
592
822
  const availableWidth = Number(output.columns || 96);
593
823
  const innerWidth = Math.max(42, Math.min(availableWidth - 8, 116));
594
- const sectionTopOffsetFromInputLine = 3;
595
824
  let renderedOnce = false;
596
825
  let value = "";
826
+ let pendingControlBuffer = "";
827
+ let previousCursorRowsFromTop = 0;
597
828
  const cleanup = () => {
598
829
  input.removeListener("data", onData);
599
830
  input.setRawMode(false);
600
831
  input.pause();
601
832
  rl.resume();
602
833
  };
603
- const renderInputContent = () => {
604
- const promptPlain = "> ";
605
- const promptStyled = style(">", `${ANSI.bold}${ANSI.white}`, enabled);
606
- const maxValueLength = Math.max(0, innerWidth - promptPlain.length);
607
- if (!value) {
608
- const placeholder = truncate(CONSOLE_PLACEHOLDER, maxValueLength);
609
- const padded = `${style(placeholder, ANSI.slate, enabled)}${" ".repeat(Math.max(0, maxValueLength - placeholder.length))}`;
610
- return `${promptStyled} ${padded}`;
611
- }
612
- const visibleValue = value.length > maxValueLength
613
- ? value.slice(value.length - maxValueLength)
614
- : value;
615
- return `${promptStyled} ${visibleValue}${" ".repeat(Math.max(0, maxValueLength - visibleValue.length))}`;
616
- };
617
834
  const render = () => {
835
+ const composer = renderConsoleComposerLines(value, innerWidth, enabled);
618
836
  if (renderedOnce) {
619
837
  cursorTo(output, 0);
620
- moveCursor(output, 0, -sectionTopOffsetFromInputLine);
838
+ moveCursor(output, 0, -previousCursorRowsFromTop);
621
839
  }
622
840
  else {
623
841
  renderedOnce = true;
624
842
  }
625
- const commands = style(`Comandos: ${CONSOLE_COMMAND_HINT}`, ANSI.slateItalic, enabled);
626
843
  const top = renderPromptFrameLine(innerWidth + 2, "┌", "┐");
627
844
  const border = style("│", ANSI.brandBlue, enabled);
628
- const middle = `${border} ${renderInputContent()} ${border}`;
629
845
  const bottom = renderPromptFrameLine(innerWidth + 2, "└", "┘");
846
+ const content = composer.renderedLines.map((line) => `${border} ${line} ${border}`).join("\n");
630
847
  output.write("\u001b[J");
631
- output.write(`${commands}\n\n${top}\n${middle}\n${bottom}\n`);
848
+ output.write(`${top}\n${content}\n${bottom}\n`);
632
849
  cursorTo(output, 0);
633
- moveCursor(output, 0, -2);
634
- const visibleValueLength = Math.min(value.length, Math.max(0, innerWidth - 2));
635
- cursorTo(output, 4 + visibleValueLength);
850
+ previousCursorRowsFromTop = 1 + composer.cursorLineIndex;
851
+ moveCursor(output, 0, -previousCursorRowsFromTop);
852
+ cursorTo(output, 4 + composer.cursorColumn);
853
+ };
854
+ const insertNewline = () => {
855
+ value = `${value}\n`;
856
+ render();
857
+ };
858
+ const submitPrompt = () => {
859
+ cleanup();
860
+ output.write("\n");
861
+ resolve(normalizeText(value));
636
862
  };
637
863
  const onData = (chunk) => {
638
- const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
639
- for (const char of Array.from(text)) {
640
- if (char === "\u0003") {
864
+ pendingControlBuffer += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
865
+ while (pendingControlBuffer.length > 0) {
866
+ if (pendingControlBuffer.startsWith("\u0003")) {
641
867
  cleanup();
642
868
  reject(createCliExitError());
643
869
  return;
644
870
  }
645
- if (char === "\r" || char === "\n") {
646
- cleanup();
647
- output.write("\n");
648
- resolve(normalizeText(value));
871
+ if (pendingControlBuffer.startsWith("\r")) {
872
+ if (pendingControlBuffer.startsWith("\r\n")) {
873
+ pendingControlBuffer = pendingControlBuffer.slice(2);
874
+ }
875
+ else {
876
+ pendingControlBuffer = pendingControlBuffer.slice(1);
877
+ }
878
+ submitPrompt();
649
879
  return;
650
880
  }
651
- if (char === "\u007f" || char === "\b") {
881
+ if (pendingControlBuffer.startsWith("\n")) {
882
+ pendingControlBuffer = pendingControlBuffer.slice(1);
883
+ insertNewline();
884
+ continue;
885
+ }
886
+ if (pendingControlBuffer.startsWith("\u007f") || pendingControlBuffer.startsWith("\b")) {
887
+ pendingControlBuffer = pendingControlBuffer.slice(1);
652
888
  value = value.slice(0, -1);
653
889
  render();
654
890
  continue;
655
891
  }
656
- if (char === "\u001b") {
892
+ const controlSequence = tryConsumeControlSequence(pendingControlBuffer);
893
+ if (controlSequence) {
894
+ if (controlSequence.action === "incomplete") {
895
+ return;
896
+ }
897
+ pendingControlBuffer = pendingControlBuffer.slice(controlSequence.consumed);
898
+ if (controlSequence.action === "newline") {
899
+ insertNewline();
900
+ }
657
901
  continue;
658
902
  }
903
+ const [char] = Array.from(pendingControlBuffer);
904
+ if (!char) {
905
+ return;
906
+ }
907
+ pendingControlBuffer = pendingControlBuffer.slice(char.length);
659
908
  value += char;
660
909
  render();
661
910
  }
@@ -838,6 +1087,30 @@ export async function runTerminalShell(options) {
838
1087
  });
839
1088
  });
840
1089
  }
1090
+ async function showRuntimeRefreshProgress(options) {
1091
+ clearScreen();
1092
+ console.log(renderBanner());
1093
+ console.log("");
1094
+ console.log(renderInfoCard([
1095
+ { text: options.title, tone: "title" },
1096
+ { text: "", tone: "muted" },
1097
+ { text: options.detail, tone: "muted" },
1098
+ ]));
1099
+ console.log("");
1100
+ const enabled = supportsAnsi();
1101
+ const width = 28;
1102
+ const steps = Math.max(8, options.steps || width);
1103
+ const delayMs = Math.max(20, options.delayMs || 45);
1104
+ for (let step = 0; step <= steps; step += 1) {
1105
+ const filled = Math.round((step / steps) * width);
1106
+ const empty = Math.max(0, width - filled);
1107
+ const bar = `${"█".repeat(filled)}${"░".repeat(empty)}`;
1108
+ const line = `[${bar}] ${Math.round((step / steps) * 100)}%`;
1109
+ output.write(`\r${style(line, ANSI.brandBlue, enabled)}`);
1110
+ await delay(delayMs);
1111
+ }
1112
+ output.write("\n\n");
1113
+ }
841
1114
  function buildConversationSummary(summary, job) {
842
1115
  const rendered = renderStructuredOutcome(job, { compact: true });
843
1116
  if (!rendered) {
@@ -849,23 +1122,8 @@ function buildConversationSummary(summary, job) {
849
1122
  return `${summary}\n${rendered}`.slice(0, 4_000).trim();
850
1123
  }
851
1124
  async function printExtensionsOverview(config) {
852
- printSection("Extensions");
853
- if (!config.installedExtensions.length) {
854
- printMuted("Nenhuma extensão instalada neste bridge.");
855
- return;
856
- }
857
- for (const extension of config.installedExtensions) {
858
- if (!isManagedBridgeExtensionSlug(extension)) {
859
- console.log(`- ${extension}`);
860
- continue;
861
- }
862
- const state = await loadManagedBridgeExtensionState(extension);
863
- const status = state ? formatManagedBridgeExtensionStatus(state.status) : "sem estado salvo";
864
- console.log(`- ${extension}: ${status}`);
865
- if (state?.notes) {
866
- printMuted(` ${truncate(state.notes, 140)}`);
867
- }
868
- }
1125
+ printSection("Extensões");
1126
+ console.log(renderInfoCard(await buildExtensionsCard(config)));
869
1127
  }
870
1128
  async function runSetupWizard(rl, options) {
871
1129
  printSection(options?.postinstall ? "Setup Inicial" : "Pairing Setup");
@@ -960,6 +1218,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
960
1218
  printSection("Console");
961
1219
  printSoft("Comandos: /help, /model [fast|thinking], /status, /clear, /exit");
962
1220
  printSoft("Bridge: otto-bridge terminal, otto-bridge extensions --install <name>, otto-bridge update");
1221
+ printSoft("Composer: Enter envia; Shift+Enter quebra linha quando suportado; Ctrl+J tambem insere newline.");
963
1222
  };
964
1223
  const handlePrompt = async (promptText) => {
965
1224
  const normalizedPrompt = normalizeText(promptText);
@@ -1111,7 +1370,7 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1111
1370
  }
1112
1371
  async function printStatusView(rl, config, runtimeSession) {
1113
1372
  printSection("Bridge Status");
1114
- renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
1373
+ console.log(renderInfoCard(buildStatusCard(config, runtimeSession)));
1115
1374
  await pauseForEnter(rl);
1116
1375
  }
1117
1376
  async function printHelpView(rl) {
@@ -1152,7 +1411,7 @@ async function pickHomeChoice(rl, paired) {
1152
1411
  `${style("2.", ANSI.brandBlue, supportsAnsi())} Terminal`,
1153
1412
  `${style("3.", ANSI.brandBlue, supportsAnsi())} Setup / parear novamente`,
1154
1413
  `${style("4.", ANSI.brandBlue, supportsAnsi())} Status detalhado`,
1155
- `${style("5.", ANSI.brandBlue, supportsAnsi())} Extensões instaladas`,
1414
+ `${style("5.", ANSI.brandBlue, supportsAnsi())} Extensões`,
1156
1415
  `${style("6.", ANSI.brandBlue, supportsAnsi())} Ajuda`,
1157
1416
  `${style("7.", ANSI.brandBlue, supportsAnsi())} Sair`,
1158
1417
  ]
@@ -1190,6 +1449,7 @@ async function pickHomeChoice(rl, paired) {
1190
1449
  export async function launchInteractiveCli(options) {
1191
1450
  let rl = await createPromptInterface();
1192
1451
  let runtimeSession = null;
1452
+ let restartRequested = false;
1193
1453
  try {
1194
1454
  let config = await loadBridgeConfig();
1195
1455
  if (!config) {
@@ -1203,7 +1463,7 @@ export async function launchInteractiveCli(options) {
1203
1463
  await runOttoConsole(rl, config, runtimeSession);
1204
1464
  }
1205
1465
  if (!config) {
1206
- return;
1466
+ return { restartRequested: false };
1207
1467
  }
1208
1468
  }
1209
1469
  runtimeSession = runtimeSession || new CliRuntimeSession(config);
@@ -1220,18 +1480,34 @@ export async function launchInteractiveCli(options) {
1220
1480
  continue;
1221
1481
  }
1222
1482
  if (choice === "terminal") {
1483
+ const snapshotBefore = await captureBridgeRuntimeSnapshot(config);
1223
1484
  rl.close();
1224
1485
  await runTerminalShell({ fromHub: true });
1225
- rl = await createPromptInterface();
1226
1486
  const refreshedConfig = await loadBridgeConfig();
1487
+ const snapshotAfter = await captureBridgeRuntimeSnapshot(refreshedConfig);
1488
+ const runtimeDiff = diffBridgeRuntimeSnapshots(snapshotBefore, snapshotAfter);
1489
+ rl = await createPromptInterface();
1227
1490
  if (!refreshedConfig) {
1228
1491
  await runtimeSession?.stop().catch(() => undefined);
1229
1492
  runtimeSession = null;
1230
1493
  printWarning("Pairing local removido no terminal. Encerrando o hub.");
1231
1494
  break;
1232
1495
  }
1233
- if (JSON.stringify(refreshedConfig) !== JSON.stringify(config)) {
1496
+ if (runtimeDiff.requiresCliRestart) {
1497
+ config = refreshedConfig;
1498
+ restartRequested = true;
1499
+ await showRuntimeRefreshProgress({
1500
+ title: "Reiniciando Otto Bridge",
1501
+ detail: "Aguarde um minuto, o Otto Bridge está reiniciando para aplicar mudanças.",
1502
+ });
1503
+ break;
1504
+ }
1505
+ if (runtimeDiff.requiresRuntimeRefresh) {
1234
1506
  config = refreshedConfig;
1507
+ await showRuntimeRefreshProgress({
1508
+ title: "Aplicando mudanças do runtime",
1509
+ detail: "Aguarde um instante, o Otto Bridge está atualizando o runtime local para aplicar mudanças.",
1510
+ });
1235
1511
  if (runtimeSession) {
1236
1512
  await runtimeSession.replaceConfig(refreshedConfig);
1237
1513
  await runtimeSession.waitForReady();
@@ -1286,6 +1562,7 @@ export async function launchInteractiveCli(options) {
1286
1562
  await runtimeSession?.stop().catch(() => undefined);
1287
1563
  rl.close();
1288
1564
  }
1565
+ return { restartRequested };
1289
1566
  }
1290
1567
  export async function runSetupCommand(options) {
1291
1568
  const rl = await createPromptInterface();
package/dist/main.js CHANGED
@@ -140,6 +140,32 @@ Examples:
140
140
  function printVersion() {
141
141
  console.log(`${BRIDGE_PACKAGE_NAME} ${BRIDGE_VERSION}`);
142
142
  }
143
+ async function restartCurrentCliProcess() {
144
+ const command = process.platform === "win32" ? "otto-bridge.cmd" : "otto-bridge";
145
+ const fallbackEntrypoint = process.argv[1];
146
+ const spawnRestart = (binary, args) => new Promise((resolve, reject) => {
147
+ const child = spawn(binary, args, {
148
+ stdio: "inherit",
149
+ env: process.env,
150
+ });
151
+ child.once("spawn", () => {
152
+ resolve();
153
+ });
154
+ child.once("error", (error) => {
155
+ reject(error);
156
+ });
157
+ });
158
+ try {
159
+ await spawnRestart(command, []);
160
+ return;
161
+ }
162
+ catch {
163
+ if (!fallbackEntrypoint) {
164
+ throw new Error("Nao foi possivel relancar o Otto Bridge apos atualizar.");
165
+ }
166
+ }
167
+ await spawnRestart(process.execPath, [fallbackEntrypoint]);
168
+ }
143
169
  function runChildCommand(command, args) {
144
170
  return new Promise((resolve, reject) => {
145
171
  const child = spawn(command, args, {
@@ -555,7 +581,9 @@ async function main() {
555
581
  const args = parseArgs(process.argv.slice(2));
556
582
  switch (args.command) {
557
583
  case "home":
558
- await launchInteractiveCli();
584
+ if ((await launchInteractiveCli()).restartRequested) {
585
+ await restartCurrentCliProcess();
586
+ }
559
587
  return;
560
588
  case "setup":
561
589
  await runSetupCommand({
@@ -572,7 +600,9 @@ async function main() {
572
600
  await runPairCommand(args);
573
601
  return;
574
602
  case "run":
575
- await launchInteractiveCli();
603
+ if ((await launchInteractiveCli()).restartRequested) {
604
+ await restartCurrentCliProcess();
605
+ }
576
606
  return;
577
607
  case "status":
578
608
  await runStatusCommand();
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "1.0.7";
2
+ export const BRIDGE_VERSION = "1.0.9";
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.7",
3
+ "version": "1.0.9",
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.7");
27
+ console.log("\n[otto-bridge] Welcome to OTTOAI 1.0.9");
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"], {