@leg3ndy/otto-bridge 1.0.2 → 1.0.4

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.2`, com runtime agentico formal, hub terminal refinado e console alinhado ao Otto da web, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_2_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_2_PATCH.md).
18
+ Para a release atual `1.0.4`, com estabilizacao do TTY, pensamento visivel no modo `Thinking` e replay web mais compacto, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_4_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_4_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.2.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.0.4.tgz
42
42
  ```
43
43
 
44
- Na linha `1.0.2`, `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.4`, `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.2` 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.4` 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.2` consolida o hub terminal como fluxo principal, esconde aliases legados da superfície pública 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.4` consolida o hub terminal como fluxo principal, estabiliza o TTY do console, deixa o modo `Thinking` visualmente claro e reduz o ruido do replay no modal web.
49
49
 
50
50
  ## Publicacao
51
51
 
@@ -137,9 +137,19 @@ otto-bridge console
137
137
 
138
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.
139
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
+ No modo `OttoAI Thinking`, o terminal agora marca explicitamente o trecho de raciocínio com `Pensando (OttoAI Thinking)` e separa esse bloco da resposta final do Otto.
147
+
148
+ 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.
149
+
140
150
  ### WhatsApp Web em background
141
151
 
142
- Fluxo recomendado na linha `1.0.2`:
152
+ Fluxo recomendado na linha `1.0.4`:
143
153
 
144
154
  ```bash
145
155
  otto-bridge extensions --install whatsappweb
@@ -149,13 +159,13 @@ otto-bridge extensions --status whatsappweb
149
159
 
150
160
  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.
151
161
 
152
- Contrato da linha `1.0.2`:
162
+ Contrato da linha `1.0.4`:
153
163
 
154
164
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
155
165
  - `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
156
166
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
157
167
 
158
- ## Handoff rapido da linha 1.0.2
168
+ ## Handoff rapido da linha 1.0.4
159
169
 
160
170
  Ja fechado no codigo:
161
171
 
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createInterface } from "node:readline/promises";
3
+ import { cursorTo, moveCursor, } from "node:readline";
3
4
  import process, { stdin as input, stdout as output } from "node:process";
4
5
  import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
5
6
  import { streamDeviceCliChat, } from "./chat_cli_client.js";
@@ -12,9 +13,12 @@ const ANSI = {
12
13
  reset: "\u001b[0m",
13
14
  dim: "\u001b[2m",
14
15
  bold: "\u001b[1m",
15
- coral: "\u001b[38;5;216m",
16
+ italic: "\u001b[3m",
17
+ brandBlue: "\u001b[38;2;0;119;208m",
16
18
  blue: "\u001b[38;5;111m",
17
19
  teal: "\u001b[38;5;80m",
20
+ slate: "\u001b[38;5;245m",
21
+ slateItalic: "\u001b[38;5;245m\u001b[3m",
18
22
  amber: "\u001b[38;5;221m",
19
23
  red: "\u001b[38;5;203m",
20
24
  green: "\u001b[38;5;114m",
@@ -29,6 +33,22 @@ const OTTOAI_BANNER = [
29
33
  " ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝",
30
34
  ];
31
35
  const CLI_EXIT_SENTINEL = "__OTTO_BRIDGE_CLI_EXIT__";
36
+ const CLI_MODEL_REGISTRY = {
37
+ fast: {
38
+ label: "OttoAI Fast",
39
+ requestModel: "deepseek-chat",
40
+ aliases: ["fast", "chat", "default", "ottoai fast", "otto fast"],
41
+ },
42
+ thinking: {
43
+ label: "OttoAI Thinking",
44
+ requestModel: "deepseek-reasoner",
45
+ aliases: ["thinking", "reasoner", "think", "ottoai thinking", "otto thinking"],
46
+ },
47
+ };
48
+ const MAX_RENDERED_LIST_ENTRIES = 28;
49
+ const MAX_RENDERED_LIST_ENTRIES_COMPACT = 10;
50
+ const MAX_RENDERED_FILE_CHARS = 6_000;
51
+ const MAX_RENDERED_FILE_CHARS_COMPACT = 1_400;
32
52
  class CliRuntimeSession {
33
53
  config;
34
54
  runtime = null;
@@ -100,6 +120,18 @@ class CliRuntimeSession {
100
120
  });
101
121
  await delay(350);
102
122
  }
123
+ async waitForReady(timeoutMs = 4500) {
124
+ const startedAt = Date.now();
125
+ for (;;) {
126
+ if (this.status !== "starting") {
127
+ return;
128
+ }
129
+ if (Date.now() - startedAt >= timeoutMs) {
130
+ return;
131
+ }
132
+ await delay(120);
133
+ }
134
+ }
103
135
  async replaceConfig(nextConfig) {
104
136
  await this.stop();
105
137
  this.config = nextConfig;
@@ -155,16 +187,23 @@ function style(text, color, enabled = true) {
155
187
  function supportsAnsi() {
156
188
  return Boolean(output.isTTY);
157
189
  }
190
+ function clearScreen() {
191
+ if (supportsAnsi()) {
192
+ output.write("\u001b[2J\u001b[3J\u001b[H");
193
+ return;
194
+ }
195
+ console.clear();
196
+ }
158
197
  function renderBanner() {
159
198
  const enabled = supportsAnsi();
160
- const lines = OTTOAI_BANNER.map((line) => style(line, ANSI.coral, enabled));
199
+ const lines = OTTOAI_BANNER.map((line) => style(line, ANSI.brandBlue, enabled));
161
200
  const title = `${BRIDGE_PACKAGE_NAME} v${BRIDGE_VERSION}`;
162
201
  const subtitle = "Paired local runtime and terminal console";
163
202
  return [
164
203
  lines.join("\n"),
165
204
  "",
166
- `${style("OTTO BRIDGE", ANSI.blue, enabled)} ${style(title, ANSI.white, enabled)}`,
167
- `${style(subtitle, ANSI.dim, enabled)}`,
205
+ `${style("OTTO BRIDGE", ANSI.brandBlue, enabled)} ${style(title, ANSI.white, enabled)}`,
206
+ `${style(subtitle, ANSI.slate, enabled)}`,
168
207
  ].join("\n");
169
208
  }
170
209
  function createCliExitError() {
@@ -182,11 +221,14 @@ function isReadlineClosedError(error) {
182
221
  }
183
222
  function printSection(title) {
184
223
  const enabled = supportsAnsi();
185
- console.log(`\n${style(title, ANSI.blue, enabled)}`);
224
+ console.log(`\n${style(title, ANSI.brandBlue, enabled)}`);
186
225
  }
187
226
  function printMuted(message) {
188
227
  console.log(style(message, ANSI.dim, supportsAnsi()));
189
228
  }
229
+ function printSoft(message) {
230
+ console.log(style(message, ANSI.slateItalic, supportsAnsi()));
231
+ }
190
232
  function printSuccess(message) {
191
233
  console.log(style(message, ANSI.green, supportsAnsi()));
192
234
  }
@@ -206,6 +248,40 @@ function truncate(text, max = 180) {
206
248
  }
207
249
  return `${value.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
208
250
  }
251
+ function humanFileSize(value) {
252
+ const size = typeof value === "number" ? value : Number(value);
253
+ if (!Number.isFinite(size) || size < 0) {
254
+ return "";
255
+ }
256
+ if (size < 1024) {
257
+ return `${size} B`;
258
+ }
259
+ if (size < 1024 * 1024) {
260
+ return `${(size / 1024).toFixed(1)} KB`;
261
+ }
262
+ if (size < 1024 * 1024 * 1024) {
263
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
264
+ }
265
+ return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
266
+ }
267
+ function getCliModelLabel(mode) {
268
+ return CLI_MODEL_REGISTRY[mode].label;
269
+ }
270
+ function getCliModelRequestModel(mode) {
271
+ return CLI_MODEL_REGISTRY[mode].requestModel;
272
+ }
273
+ export function resolveCliModelMode(value) {
274
+ const normalized = normalizeText(value).toLowerCase();
275
+ if (!normalized) {
276
+ return null;
277
+ }
278
+ for (const [mode, definition] of Object.entries(CLI_MODEL_REGISTRY)) {
279
+ if (definition.aliases.includes(normalized)) {
280
+ return mode;
281
+ }
282
+ }
283
+ return null;
284
+ }
209
285
  function delay(ms) {
210
286
  return new Promise((resolve) => setTimeout(resolve, ms));
211
287
  }
@@ -234,7 +310,7 @@ async function question(rl, prompt) {
234
310
  async function ask(rl, label, options) {
235
311
  const defaultValue = normalizeText(options?.defaultValue);
236
312
  const suffix = defaultValue ? ` ${style(`[${defaultValue}]`, ANSI.dim, supportsAnsi())}` : "";
237
- const answer = normalizeText(await question(rl, `${style("›", ANSI.coral, supportsAnsi())} ${label}${suffix}: `));
313
+ const answer = normalizeText(await question(rl, `${style("›", ANSI.brandBlue, supportsAnsi())} ${label}${suffix}: `));
238
314
  if (answer) {
239
315
  return answer;
240
316
  }
@@ -247,7 +323,7 @@ async function ask(rl, label, options) {
247
323
  return await ask(rl, label, options);
248
324
  }
249
325
  async function askYesNo(rl, promptText, defaultValue = true) {
250
- const answer = normalizeText(await question(rl, `${style("?", ANSI.blue, supportsAnsi())} ${promptText} ${style(defaultValue ? "[Y/n]" : "[y/N]", ANSI.dim, supportsAnsi())}: `)).toLowerCase();
326
+ const answer = normalizeText(await question(rl, `${style("?", ANSI.brandBlue, supportsAnsi())} ${promptText} ${style(defaultValue ? "[Y/n]" : "[y/N]", ANSI.dim, supportsAnsi())}: `)).toLowerCase();
251
327
  if (!answer) {
252
328
  return defaultValue;
253
329
  }
@@ -259,8 +335,8 @@ async function pauseForEnter(rl, message = "Pressione Enter para continuar") {
259
335
  async function chooseExecutor(rl, current) {
260
336
  const defaultType = current?.type || resolveExecutorConfig().type;
261
337
  console.log([
262
- `${style("1.", ANSI.coral, supportsAnsi())} native-macos ${style("(Mac real, runtime local)", ANSI.dim, supportsAnsi())}`,
263
- `${style("2.", ANSI.coral, supportsAnsi())} mock ${style("(ambiente de teste)", ANSI.dim, supportsAnsi())}`,
338
+ `${style("1.", ANSI.brandBlue, supportsAnsi())} native-macos ${style("(Mac real, runtime local)", ANSI.dim, supportsAnsi())}`,
339
+ `${style("2.", ANSI.brandBlue, supportsAnsi())} mock ${style("(ambiente de teste)", ANSI.dim, supportsAnsi())}`,
264
340
  ].join("\n"));
265
341
  const selection = await ask(rl, "Executor", {
266
342
  defaultValue: defaultType === "mock" ? "2" : "1",
@@ -322,16 +398,16 @@ function extractConfirmationPrompt(job) {
322
398
  }
323
399
  function renderStatusOverview(config, runtimeSession) {
324
400
  return [
325
- `${style("Device", ANSI.blue, supportsAnsi())}: ${config.deviceName}`,
326
- `${style("Device ID", ANSI.blue, supportsAnsi())}: ${config.deviceId}`,
327
- `${style("API", ANSI.blue, supportsAnsi())}: ${config.apiBaseUrl}`,
328
- `${style("Executor", ANSI.blue, supportsAnsi())}: ${config.executor.type}`,
329
- `${style("Approval", ANSI.blue, supportsAnsi())}: ${config.approvalMode}`,
330
- `${style("Runtime", ANSI.blue, supportsAnsi())}: ${runtimeSession?.getStatusLabel() || "offline"}`,
401
+ `${style("Device", ANSI.brandBlue, supportsAnsi())}: ${config.deviceName}`,
402
+ `${style("Device ID", ANSI.brandBlue, supportsAnsi())}: ${config.deviceId}`,
403
+ `${style("API", ANSI.brandBlue, supportsAnsi())}: ${config.apiBaseUrl}`,
404
+ `${style("Executor", ANSI.brandBlue, supportsAnsi())}: ${config.executor.type}`,
405
+ `${style("Approval", ANSI.brandBlue, supportsAnsi())}: ${config.approvalMode}`,
406
+ `${style("Runtime", ANSI.brandBlue, supportsAnsi())}: ${runtimeSession?.getStatusLabel() || "offline"}`,
331
407
  ...(runtimeSession?.getStatusDetail()
332
- ? [`${style("Runtime note", ANSI.blue, supportsAnsi())}: ${runtimeSession.getStatusDetail()}`]
408
+ ? [`${style("Runtime note", ANSI.brandBlue, supportsAnsi())}: ${runtimeSession.getStatusDetail()}`]
333
409
  : []),
334
- `${style("Config", ANSI.blue, supportsAnsi())}: ${getBridgeConfigPath()}`,
410
+ `${style("Config", ANSI.brandBlue, supportsAnsi())}: ${getBridgeConfigPath()}`,
335
411
  ];
336
412
  }
337
413
  function renderRuntimeHeadline(runtimeSession) {
@@ -356,49 +432,228 @@ function renderRuntimeHeadline(runtimeSession) {
356
432
  function padRight(text, width) {
357
433
  return text.length >= width ? text : `${text}${" ".repeat(width - text.length)}`;
358
434
  }
435
+ function styleCardLine(line, tone, enabled) {
436
+ if (!enabled) {
437
+ return line;
438
+ }
439
+ if (tone === "title") {
440
+ return `${ANSI.bold}${ANSI.brandBlue}${line}${ANSI.reset}`;
441
+ }
442
+ if (tone === "muted") {
443
+ return `${ANSI.slateItalic}${line}${ANSI.reset}`;
444
+ }
445
+ return `${ANSI.white}${line}${ANSI.reset}`;
446
+ }
359
447
  function renderInfoCard(lines) {
360
- const normalized = lines.map((line) => truncate(line, 78));
361
- const width = Math.max(38, ...normalized.map((line) => line.length));
448
+ const enabled = supportsAnsi();
449
+ const normalized = lines.map((line) => ({
450
+ text: truncate(line.text, 82),
451
+ tone: line.tone,
452
+ }));
453
+ const width = Math.max(44, ...normalized.map((line) => line.text.length));
454
+ const top = style(`┌${"─".repeat(width + 2)}┐`, ANSI.brandBlue, enabled);
455
+ const bottom = style(`└${"─".repeat(width + 2)}┘`, ANSI.brandBlue, enabled);
362
456
  return [
363
- `+${"-".repeat(width + 2)}+`,
364
- ...normalized.map((line) => `| ${padRight(line, width)} |`),
365
- `+${"-".repeat(width + 2)}+`,
457
+ top,
458
+ ...normalized.map((line) => {
459
+ const border = style("", ANSI.brandBlue, enabled);
460
+ const content = styleCardLine(padRight(line.text, width), line.tone, enabled);
461
+ return `${border} ${content} ${border}`;
462
+ }),
463
+ bottom,
366
464
  ].join("\n");
367
465
  }
368
- function printHubScreen(runtimeSession) {
369
- console.clear();
466
+ function buildWelcomeCard(runtimeSession, modelMode) {
467
+ return [
468
+ { text: "Welcome to OttoAI", tone: "title" },
469
+ { text: "", tone: "muted" },
470
+ { text: "/help inside the console, /status for bridge details", tone: "muted" },
471
+ { text: "", tone: "muted" },
472
+ { text: `model: ${getCliModelLabel(modelMode)}`, tone: "muted" },
473
+ { text: `cwd: ${process.cwd()}`, tone: "muted" },
474
+ { text: renderRuntimeHeadline(runtimeSession), tone: "muted" },
475
+ ];
476
+ }
477
+ function printHubScreen(runtimeSession, modelMode) {
478
+ clearScreen();
370
479
  console.log(renderBanner());
371
480
  console.log("");
372
- console.log(renderInfoCard([
373
- "Welcome to OttoAI",
374
- "/status for bridge details, /help inside the console",
375
- "model: OttoAI local console",
376
- `cwd: ${process.cwd()}`,
377
- renderRuntimeHeadline(runtimeSession),
378
- ]));
481
+ console.log(renderInfoCard(buildWelcomeCard(runtimeSession, modelMode)));
379
482
  }
380
- function printConsoleScreen(runtimeSession) {
381
- console.clear();
483
+ function printConsoleScreen(runtimeSession, modelMode) {
484
+ clearScreen();
382
485
  console.log(renderBanner());
383
486
  console.log("");
384
- console.log(renderInfoCard([
385
- "Welcome to OttoAI",
386
- "/help for commands, /status for bridge details",
387
- "model: OttoAI local console",
388
- `cwd: ${process.cwd()}`,
389
- renderRuntimeHeadline(runtimeSession),
390
- ]));
487
+ console.log(renderInfoCard(buildWelcomeCard(runtimeSession, modelMode)));
488
+ console.log("");
489
+ console.log(style("Peça algo ao Otto", `${ANSI.bold}${ANSI.white}`, supportsAnsi()));
490
+ printSoft("Comandos: /help, /model [fast|thinking], /status, /clear, /exit");
391
491
  console.log("");
392
492
  }
493
+ function renderPromptFrameLine(width, edgeLeft, edgeRight) {
494
+ return style(`${edgeLeft}${"─".repeat(width)}${edgeRight}`, ANSI.brandBlue, supportsAnsi());
495
+ }
393
496
  async function askConsoleInput(rl) {
394
- return normalizeText(await question(rl, `${style(">", ANSI.white, supportsAnsi())} `));
497
+ if (!supportsAnsi()) {
498
+ return normalizeText(await question(rl, "> "));
499
+ }
500
+ const availableWidth = Number(output.columns || 96);
501
+ const inputWidth = Math.max(28, Math.min(availableWidth - 12, 92));
502
+ const top = renderPromptFrameLine(inputWidth + 4, "┌", "┐");
503
+ const bottom = renderPromptFrameLine(inputWidth + 4, "└", "┘");
504
+ const middle = [
505
+ style("│", ANSI.brandBlue, true),
506
+ " ",
507
+ style(">", `${ANSI.bold}${ANSI.white}`, true),
508
+ " ",
509
+ " ".repeat(inputWidth),
510
+ " ",
511
+ style("│", ANSI.brandBlue, true),
512
+ ].join("");
513
+ output.write(`${top}\n${middle}\n${bottom}`);
514
+ cursorTo(output, 0);
515
+ moveCursor(output, 0, -1);
516
+ cursorTo(output, 4);
517
+ const answer = normalizeText(await question(rl, ""));
518
+ output.write("\n");
519
+ return answer;
395
520
  }
396
521
  function printAssistantMessage(message) {
397
522
  const text = normalizeText(message);
398
523
  if (!text) {
399
524
  return;
400
525
  }
401
- console.log(`${style("•", ANSI.coral, supportsAnsi())} ${text}`);
526
+ console.log(`${style("•", ANSI.brandBlue, supportsAnsi())} ${text}`);
527
+ }
528
+ function extractJobOutcome(job) {
529
+ const result = job.result && typeof job.result === "object"
530
+ ? job.result
531
+ : {};
532
+ const outcome = result.outcome && typeof result.outcome === "object"
533
+ ? result.outcome
534
+ : {};
535
+ if (Object.keys(outcome).length > 0) {
536
+ return outcome;
537
+ }
538
+ const payload = job.payload && typeof job.payload === "object"
539
+ ? job.payload
540
+ : {};
541
+ const actions = Array.isArray(payload.actions) ? payload.actions : [];
542
+ const firstAction = actions.find((item) => item && typeof item === "object");
543
+ const actionType = normalizeText(firstAction?.type).toLowerCase();
544
+ if (actionType === "list_files" && result.file_listing && typeof result.file_listing === "object") {
545
+ const listing = result.file_listing;
546
+ return {
547
+ action_type: "list_files",
548
+ path: normalizeText(firstAction?.path),
549
+ resolved_path: normalizeText(listing.resolved_path),
550
+ listed_item_count: listing.item_count,
551
+ total_item_count: listing.total_item_count,
552
+ limit_applied: listing.limit_applied,
553
+ entries: Array.isArray(listing.entries) ? listing.entries : [],
554
+ };
555
+ }
556
+ if (actionType === "read_file" && result.read_file && typeof result.read_file === "object") {
557
+ const readFile = result.read_file;
558
+ return {
559
+ action_type: "read_file",
560
+ path: normalizeText(firstAction?.path),
561
+ resolved_path: normalizeText(readFile.resolved_path),
562
+ content: typeof readFile.content === "string" ? readFile.content : undefined,
563
+ content_chunks: Array.isArray(readFile.chunks) ? readFile.chunks : [],
564
+ binary_notice: normalizeText(readFile.binary_notice),
565
+ };
566
+ }
567
+ return {};
568
+ }
569
+ function outcomePathLabel(outcome) {
570
+ return normalizeText(outcome.resolved_path || outcome.path);
571
+ }
572
+ function collectOutcomeFileContent(outcome) {
573
+ const directContent = outcome.content;
574
+ if (typeof directContent === "string" && directContent.trim()) {
575
+ return directContent;
576
+ }
577
+ const chunks = Array.isArray(outcome.content_chunks) ? outcome.content_chunks : [];
578
+ return chunks
579
+ .filter((item) => item && typeof item === "object")
580
+ .map((item) => String(item.text || ""))
581
+ .filter((item) => item.length > 0)
582
+ .join("");
583
+ }
584
+ function formatDirectoryEntry(entry) {
585
+ const name = normalizeText(entry.name) || "(sem nome)";
586
+ const kind = normalizeText(entry.kind).toLowerCase();
587
+ const prefix = kind === "directory" ? "[dir]" : "[file]";
588
+ const size = kind === "file" ? humanFileSize(entry.size_bytes) : "";
589
+ return `${prefix} ${name}${size ? ` (${size})` : ""}`;
590
+ }
591
+ export function renderStructuredOutcome(job, options) {
592
+ const compact = Boolean(options?.compact);
593
+ const outcome = extractJobOutcome(job);
594
+ const actionType = normalizeText(outcome.action_type).toLowerCase();
595
+ const lines = [];
596
+ if (actionType === "list_files") {
597
+ const entries = Array.isArray(outcome.entries)
598
+ ? outcome.entries.filter((item) => item && typeof item === "object")
599
+ : [];
600
+ const target = outcomePathLabel(outcome);
601
+ const itemCount = outcome.listed_item_count ?? entries.length;
602
+ if (target) {
603
+ lines.push(target);
604
+ }
605
+ if (itemCount) {
606
+ lines.push(`${itemCount} item(ns) encontrados`);
607
+ }
608
+ if (lines.length) {
609
+ lines.push("");
610
+ }
611
+ const maxEntries = compact ? MAX_RENDERED_LIST_ENTRIES_COMPACT : MAX_RENDERED_LIST_ENTRIES;
612
+ entries.slice(0, maxEntries).forEach((entry) => {
613
+ lines.push(formatDirectoryEntry(entry));
614
+ });
615
+ if (entries.length > maxEntries) {
616
+ lines.push(`... +${entries.length - maxEntries} item(ns)`);
617
+ }
618
+ }
619
+ if (actionType === "read_file") {
620
+ const target = outcomePathLabel(outcome);
621
+ const binaryNotice = normalizeText(outcome.binary_notice);
622
+ const content = collectOutcomeFileContent(outcome);
623
+ const maxChars = compact ? MAX_RENDERED_FILE_CHARS_COMPACT : MAX_RENDERED_FILE_CHARS;
624
+ if (target) {
625
+ lines.push(target);
626
+ lines.push("");
627
+ }
628
+ if (binaryNotice) {
629
+ lines.push(binaryNotice);
630
+ }
631
+ else if (content) {
632
+ const truncatedContent = content.length > maxChars
633
+ ? `${content.slice(0, maxChars).trimEnd()}\n\n[... conteúdo truncado ...]`
634
+ : content;
635
+ lines.push(truncatedContent);
636
+ }
637
+ }
638
+ return lines.join("\n").trim();
639
+ }
640
+ function printStructuredOutcome(job) {
641
+ const rendered = renderStructuredOutcome(job);
642
+ if (!rendered) {
643
+ return;
644
+ }
645
+ console.log("");
646
+ console.log(rendered);
647
+ }
648
+ function buildConversationSummary(summary, job) {
649
+ const rendered = renderStructuredOutcome(job, { compact: true });
650
+ if (!rendered) {
651
+ return summary;
652
+ }
653
+ if (!summary) {
654
+ return rendered;
655
+ }
656
+ return `${summary}\n${rendered}`.slice(0, 4_000).trim();
402
657
  }
403
658
  async function printExtensionsOverview(config) {
404
659
  printSection("Extensions");
@@ -497,18 +752,19 @@ async function followConsoleJob(rl, config, jobId) {
497
752
  ? "Execução local falhou."
498
753
  : "Execução local cancelada.");
499
754
  printAssistantMessage(summary);
500
- return summary;
755
+ printStructuredOutcome(job);
756
+ return buildConversationSummary(summary, job);
501
757
  }
502
758
  await delay(1400);
503
759
  }
504
760
  }
505
761
  async function runOttoConsole(rl, config, runtimeSession, options) {
506
- printConsoleScreen(runtimeSession);
507
- printMuted("Digite sua mensagem normalmente. O handoff local só acontece quando fizer sentido.");
762
+ let activeModel = "fast";
763
+ printConsoleScreen(runtimeSession, activeModel);
508
764
  const sessionId = randomUUID();
509
765
  const conversation = [];
510
766
  const printConsoleHelp = () => {
511
- printMuted("Comandos: /help, /clear, /status, /exit");
767
+ printSoft("Comandos: /help, /model [fast|thinking], /status, /clear, /exit");
512
768
  };
513
769
  const handlePrompt = async (promptText) => {
514
770
  const normalizedPrompt = normalizeText(promptText);
@@ -521,11 +777,32 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
521
777
  }
522
778
  if (normalizedPrompt === "/clear") {
523
779
  conversation.splice(0, conversation.length);
524
- printConsoleScreen(runtimeSession);
780
+ printConsoleScreen(runtimeSession, activeModel);
525
781
  printMuted("Contexto local do console limpo.");
526
782
  return;
527
783
  }
784
+ if (normalizedPrompt === "/model") {
785
+ printMuted(`Modelo ativo: ${getCliModelLabel(activeModel)}.`);
786
+ printSoft("Use /model fast ou /model thinking para trocar.");
787
+ return;
788
+ }
789
+ if (normalizedPrompt.startsWith("/model ")) {
790
+ const nextMode = resolveCliModelMode(normalizedPrompt.slice("/model ".length));
791
+ if (!nextMode) {
792
+ printWarning("Modelo inválido. Use /model fast ou /model thinking.");
793
+ return;
794
+ }
795
+ if (nextMode === activeModel) {
796
+ printMuted(`Modelo já está em ${getCliModelLabel(activeModel)}.`);
797
+ return;
798
+ }
799
+ activeModel = nextMode;
800
+ printConsoleScreen(runtimeSession, activeModel);
801
+ printSuccess(`Modelo ativo: ${getCliModelLabel(activeModel)}.`);
802
+ return;
803
+ }
528
804
  if (normalizedPrompt === "/status") {
805
+ console.log(`${style("Model", ANSI.brandBlue, supportsAnsi())}: ${getCliModelLabel(activeModel)}`);
529
806
  renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
530
807
  const runtimeFailure = runtimeSession.getLastError();
531
808
  if (runtimeFailure) {
@@ -542,9 +819,12 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
542
819
  }
543
820
  let streamedAssistant = "";
544
821
  let assistantPrefixPrinted = false;
822
+ let reasoningPrefixPrinted = false;
823
+ let contentSeparatedFromReasoning = false;
545
824
  let handoffPayload = null;
546
825
  await streamDeviceCliChat(config, {
547
826
  messages: conversation,
827
+ model: getCliModelRequestModel(activeModel),
548
828
  session_id: sessionId,
549
829
  }, async (event) => {
550
830
  const chunkType = normalizeText(event.chunk_type).toLowerCase();
@@ -564,12 +844,27 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
564
844
  if (errorMessage) {
565
845
  throw new Error(errorMessage);
566
846
  }
847
+ const reasoningChunk = typeof event.content === "string" && eventType === "reasoning"
848
+ ? event.content
849
+ : "";
850
+ if (reasoningChunk) {
851
+ if (!reasoningPrefixPrinted) {
852
+ output.write(`${style("Pensando (OttoAI Thinking)\n", ANSI.brandBlue, supportsAnsi())}`);
853
+ reasoningPrefixPrinted = true;
854
+ }
855
+ output.write(style(reasoningChunk, ANSI.slateItalic, supportsAnsi()));
856
+ return;
857
+ }
567
858
  const contentChunk = typeof event.content === "string" ? event.content : "";
568
859
  if (!contentChunk) {
569
860
  return;
570
861
  }
862
+ if (reasoningPrefixPrinted && !contentSeparatedFromReasoning) {
863
+ output.write("\n\n");
864
+ contentSeparatedFromReasoning = true;
865
+ }
571
866
  if (!assistantPrefixPrinted) {
572
- output.write(`${style("•", ANSI.coral, supportsAnsi())} `);
867
+ output.write(`${style("•", ANSI.brandBlue, supportsAnsi())} `);
573
868
  assistantPrefixPrinted = true;
574
869
  }
575
870
  output.write(contentChunk);
@@ -582,13 +877,13 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
582
877
  if (handoffPayload) {
583
878
  const handoffData = handoffPayload;
584
879
  const bridgeSummary = extractBridgeHandoffSummary(handoffData);
585
- if (bridgeSummary) {
586
- printAssistantMessage(bridgeSummary);
587
- }
588
880
  const job = handoffData.job && typeof handoffData.job === "object"
589
881
  ? handoffData.job
590
882
  : null;
591
883
  const jobId = normalizeText(job?.id);
884
+ if (bridgeSummary && !jobId) {
885
+ printAssistantMessage(bridgeSummary);
886
+ }
592
887
  if (jobId) {
593
888
  finalAssistantSummary = await followConsoleJob(rl, config, jobId);
594
889
  }
@@ -603,7 +898,6 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
603
898
  }
604
899
  }
605
900
  };
606
- printConsoleHelp();
607
901
  if (options?.initialPrompt) {
608
902
  await handlePrompt(options.initialPrompt);
609
903
  }
@@ -623,27 +917,50 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
623
917
  async function printStatusView(rl, config, runtimeSession) {
624
918
  printSection("Bridge Status");
625
919
  renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
626
- await printExtensionsOverview(config);
920
+ await pauseForEnter(rl);
921
+ }
922
+ async function printHelpView(rl) {
923
+ printSection("Ajuda");
924
+ console.log(renderInfoCard([
925
+ { text: "Otto Bridge CLI", tone: "title" },
926
+ { text: "", tone: "muted" },
927
+ { text: "otto-bridge", tone: "primary" },
928
+ { text: "Abre o hub principal e sobe o runtime local automaticamente.", tone: "muted" },
929
+ { text: "", tone: "muted" },
930
+ { text: "otto-bridge setup", tone: "primary" },
931
+ { text: "Repareia o dispositivo e atualiza config/executor.", tone: "muted" },
932
+ { text: "", tone: "muted" },
933
+ { text: "otto-bridge console", tone: "primary" },
934
+ { text: "Abre direto o console do Otto no terminal.", tone: "muted" },
935
+ { text: "", tone: "muted" },
936
+ { text: "Dentro do console: /help, /model fast|thinking, /status, /clear, /exit", tone: "muted" },
937
+ ]));
627
938
  await pauseForEnter(rl);
628
939
  }
629
940
  async function pickHomeChoice(rl, paired) {
630
941
  printSection("Home");
631
942
  const options = paired
632
943
  ? [
633
- `${style("1.", ANSI.coral, supportsAnsi())} Otto Console`,
634
- `${style("2.", ANSI.coral, supportsAnsi())} Setup / parear novamente`,
635
- `${style("3.", ANSI.coral, supportsAnsi())} Status detalhado`,
636
- `${style("4.", ANSI.coral, supportsAnsi())} Extensões instaladas`,
637
- `${style("5.", ANSI.coral, supportsAnsi())} Sair`,
944
+ `${style("1.", ANSI.brandBlue, supportsAnsi())} Otto Console`,
945
+ `${style("2.", ANSI.brandBlue, supportsAnsi())} Setup / parear novamente`,
946
+ `${style("3.", ANSI.brandBlue, supportsAnsi())} Status detalhado`,
947
+ `${style("4.", ANSI.brandBlue, supportsAnsi())} Extensões instaladas`,
948
+ `${style("5.", ANSI.brandBlue, supportsAnsi())} Sair`,
949
+ `${style("6.", ANSI.brandBlue, supportsAnsi())} Ajuda`,
638
950
  ]
639
951
  : [
640
- `${style("1.", ANSI.coral, supportsAnsi())} Pairing setup`,
641
- `${style("2.", ANSI.coral, supportsAnsi())} Sair`,
952
+ `${style("1.", ANSI.brandBlue, supportsAnsi())} Pairing setup`,
953
+ `${style("2.", ANSI.brandBlue, supportsAnsi())} Sair`,
954
+ `${style("3.", ANSI.brandBlue, supportsAnsi())} Ajuda`,
642
955
  ];
643
956
  console.log(options.join("\n"));
644
957
  const answer = await ask(rl, "Escolha");
645
958
  if (!paired) {
646
- return answer === "1" ? "setup" : "exit";
959
+ if (answer === "1")
960
+ return "setup";
961
+ if (answer === "3")
962
+ return "help";
963
+ return "exit";
647
964
  }
648
965
  if (answer === "1")
649
966
  return "console";
@@ -653,6 +970,8 @@ async function pickHomeChoice(rl, paired) {
653
970
  return "status";
654
971
  if (answer === "4")
655
972
  return "extensions";
973
+ if (answer === "6")
974
+ return "help";
656
975
  return "exit";
657
976
  }
658
977
  export async function launchInteractiveCli(options) {
@@ -661,12 +980,13 @@ export async function launchInteractiveCli(options) {
661
980
  try {
662
981
  let config = await loadBridgeConfig();
663
982
  if (!config) {
664
- printHubScreen(null);
983
+ printHubScreen(null, "fast");
665
984
  const setup = await runSetupWizard(rl, options);
666
985
  config = setup.config;
667
986
  if (config && setup.openConsole) {
668
987
  runtimeSession = new CliRuntimeSession(config);
669
988
  await runtimeSession.ensureStarted();
989
+ await runtimeSession.waitForReady();
670
990
  await runOttoConsole(rl, config, runtimeSession);
671
991
  }
672
992
  if (!config) {
@@ -675,22 +995,29 @@ export async function launchInteractiveCli(options) {
675
995
  }
676
996
  runtimeSession = runtimeSession || new CliRuntimeSession(config);
677
997
  await runtimeSession.ensureStarted();
998
+ await runtimeSession.waitForReady();
678
999
  for (;;) {
679
- printHubScreen(runtimeSession);
1000
+ printHubScreen(runtimeSession, "fast");
680
1001
  const choice = await pickHomeChoice(rl, true);
681
1002
  if (choice === "exit") {
682
1003
  break;
683
1004
  }
1005
+ if (choice === "help") {
1006
+ await printHelpView(rl);
1007
+ continue;
1008
+ }
684
1009
  if (choice === "setup") {
685
1010
  const setup = await runSetupWizard(rl);
686
1011
  if (setup.config) {
687
1012
  config = setup.config;
688
1013
  if (runtimeSession) {
689
1014
  await runtimeSession.replaceConfig(setup.config);
1015
+ await runtimeSession.waitForReady();
690
1016
  }
691
1017
  else {
692
1018
  runtimeSession = new CliRuntimeSession(setup.config);
693
1019
  await runtimeSession.ensureStarted();
1020
+ await runtimeSession.waitForReady();
694
1021
  }
695
1022
  }
696
1023
  if (setup.config && setup.openConsole && runtimeSession) {
@@ -726,11 +1053,12 @@ export async function runSetupCommand(options) {
726
1053
  const rl = await createPromptInterface();
727
1054
  let runtimeSession = null;
728
1055
  try {
729
- printHubScreen(null);
1056
+ printHubScreen(null, "fast");
730
1057
  const setup = await runSetupWizard(rl, options);
731
1058
  if (setup.config && setup.openConsole) {
732
1059
  runtimeSession = new CliRuntimeSession(setup.config);
733
1060
  await runtimeSession.ensureStarted();
1061
+ await runtimeSession.waitForReady();
734
1062
  await runOttoConsole(rl, setup.config, runtimeSession);
735
1063
  }
736
1064
  }
@@ -748,7 +1076,7 @@ export async function runConsoleCommand(initialPrompt) {
748
1076
  const rl = await createPromptInterface();
749
1077
  let runtimeSession = null;
750
1078
  try {
751
- printHubScreen(null);
1079
+ printHubScreen(null, "fast");
752
1080
  let config = await loadBridgeConfig();
753
1081
  if (!config) {
754
1082
  const setup = await runSetupWizard(rl);
@@ -759,6 +1087,7 @@ export async function runConsoleCommand(initialPrompt) {
759
1087
  }
760
1088
  runtimeSession = new CliRuntimeSession(config);
761
1089
  await runtimeSession.ensureStarted();
1090
+ await runtimeSession.waitForReady();
762
1091
  await runOttoConsole(rl, config, runtimeSession, { initialPrompt });
763
1092
  }
764
1093
  catch (error) {
package/dist/main.js CHANGED
@@ -104,28 +104,31 @@ function resolveExecutorOverrides(args, current) {
104
104
  }
105
105
  function printUsage() {
106
106
  console.log(`Usage:
107
- otto-bridge
108
- otto-bridge setup
109
- otto-bridge console
110
- otto-bridge status
107
+ otto-bridge abre o hub principal e sobe o runtime local
108
+ otto-bridge setup refaz o pairing no terminal
109
+ otto-bridge console abre direto o console do Otto
110
+ otto-bridge status mostra o pairing atual em JSON
111
111
  otto-bridge extensions --list
112
112
  otto-bridge extensions --install <name>
113
113
  otto-bridge extensions --setup <name>
114
114
  otto-bridge extensions --status <name>
115
115
  otto-bridge extensions --uninstall <name>
116
- otto-bridge version
117
116
  otto-bridge update [--tag latest|next] [--dry-run]
118
117
  otto-bridge unpair
118
+ otto-bridge version
119
+
120
+ Console:
121
+ /help
122
+ /model fast
123
+ /model thinking
124
+ /status
125
+ /clear
126
+ /exit
119
127
 
120
128
  Examples:
121
129
  otto-bridge
122
- otto-bridge setup
123
- otto-bridge console
124
130
  otto-bridge extensions --install <name>
125
131
  otto-bridge extensions --setup <name>
126
- otto-bridge extensions --status <name>
127
- otto-bridge extensions --list
128
- otto-bridge version
129
132
  otto-bridge update --dry-run
130
133
  otto-bridge --version`);
131
134
  }
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";
2
+ export const BRIDGE_VERSION = "1.0.4";
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.2",
3
+ "version": "1.0.4",
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.2");
27
+ console.log("\n[otto-bridge] Welcome to OTTOAI 1.0.4");
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"], {