@leg3ndy/otto-bridge 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,8 @@ 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 o patch atual `0.9.1`, com hotfixes de stream/renderizacao e completude das filesystem tools, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_1_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_1_PATCH.md).
19
+
18
20
  ## Distribuicao
19
21
 
20
22
  Fluxo recomendado agora:
@@ -35,14 +37,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
35
37
 
36
38
  ```bash
37
39
  npm pack
38
- npm install -g ./leg3ndy-otto-bridge-0.9.0.tgz
40
+ npm install -g ./leg3ndy-otto-bridge-0.9.1.tgz
39
41
  ```
40
42
 
41
- No `0.9.0`, `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.
43
+ Na linha `0.9.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.
42
44
 
43
- No macOS, o `0.9.0` 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`.
45
+ No macOS, a linha `0.9.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`.
44
46
 
45
- No nivel arquitetural, o `0.9.0` muda o papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat.
47
+ 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 `0.9.1` consolida esse caminho com hotfixes de stream visivel, planner LLM-first e leitura/listagem local sem truncamento silencioso.
46
48
 
47
49
  ## Publicacao
48
50
 
@@ -112,7 +114,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
112
114
 
113
115
  ### WhatsApp Web em background
114
116
 
115
- Fluxo recomendado no `0.9.0`:
117
+ Fluxo recomendado na linha `0.9.1`:
116
118
 
117
119
  ```bash
118
120
  otto-bridge extensions --install whatsappweb
@@ -122,13 +124,13 @@ otto-bridge extensions --status whatsappweb
122
124
 
123
125
  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.
124
126
 
125
- Contrato do `0.9.0`:
127
+ Contrato da linha `0.9.1`:
126
128
 
127
129
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
128
130
  - `otto-bridge run`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime estiver ativo, sem depender de uma aba aberta no Safari
129
131
  - ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
130
132
 
131
- ## Handoff rapido do 0.9.0
133
+ ## Handoff rapido da linha 0.9.1
132
134
 
133
135
  Ja fechado no codigo:
134
136
 
@@ -138,6 +140,9 @@ Ja fechado no codigo:
138
140
  - prompt bridge-aware no chat normal para ajudar o Otto a responder com base no que realmente aconteceu no device
139
141
  - runtime local agora publica `local_tools` para o Otto/backend saberem exatamente o que o device consegue fazer
140
142
  - o caminho principal do bridge usa `summary`/`narration_context` no lugar de resposta automatica pronta
143
+ - `read_file` agora entrega conteudo completo segmentado em `content_chunks` para o Otto
144
+ - `list_files` sem `limit` agora lista o diretorio inteiro, sem fallback silencioso para 40 itens
145
+ - o planner do bridge prioriza o modelo para escolher a tool certa pelo contexto; regex fica como fallback
141
146
 
142
147
  Ainda precisa reteste em campo:
143
148
 
@@ -702,6 +702,35 @@ function clipTextPreview(value, maxLength) {
702
702
  }
703
703
  return `${value.slice(0, maxLength)}\n\n[conteudo truncado: mostrando ${maxLength} de ${value.length} caracteres. Peca um trecho mais especifico se quiser continuar.]`;
704
704
  }
705
+ function chunkTextForTransport(value, chunkSize) {
706
+ const normalized = String(value || "");
707
+ if (!normalized) {
708
+ return [];
709
+ }
710
+ const safeChunkSize = Math.max(400, Math.min(Math.round(chunkSize || 4000), 32_000));
711
+ const chunks = [];
712
+ let cursor = 0;
713
+ let index = 0;
714
+ while (cursor < normalized.length) {
715
+ let nextCursor = Math.min(normalized.length, cursor + safeChunkSize);
716
+ if (nextCursor < normalized.length) {
717
+ const lastLineBreak = normalized.lastIndexOf("\n", nextCursor);
718
+ if (lastLineBreak > cursor + Math.floor(safeChunkSize * 0.35)) {
719
+ nextCursor = lastLineBreak + 1;
720
+ }
721
+ }
722
+ const text = normalized.slice(cursor, nextCursor);
723
+ chunks.push({
724
+ index,
725
+ start_char: cursor,
726
+ end_char: nextCursor,
727
+ text,
728
+ });
729
+ cursor = nextCursor;
730
+ index += 1;
731
+ }
732
+ return chunks;
733
+ }
705
734
  const TEXTUTIL_READABLE_EXTENSIONS = new Set([
706
735
  ".doc",
707
736
  ".docx",
@@ -975,14 +1004,14 @@ function parseStructuredActions(job) {
975
1004
  if (type === "read_file" || type === "read_local_file") {
976
1005
  const filePath = asString(action.path);
977
1006
  if (filePath) {
978
- const maxChars = typeof action.max_chars === "number" ? Math.max(200, Math.min(12000, action.max_chars)) : undefined;
1007
+ const maxChars = typeof action.max_chars === "number" ? Math.max(400, Math.min(32_000, action.max_chars)) : undefined;
979
1008
  actions.push({ type: "read_file", path: filePath, max_chars: maxChars });
980
1009
  }
981
1010
  continue;
982
1011
  }
983
1012
  if (type === "list_files" || type === "ls") {
984
1013
  const filePath = asString(action.path) || "~";
985
- const limit = typeof action.limit === "number" ? Math.max(1, Math.min(200, action.limit)) : undefined;
1014
+ const limit = typeof action.limit === "number" ? Math.max(1, Math.min(5_000, action.limit)) : undefined;
986
1015
  actions.push({ type: "list_files", path: filePath, limit });
987
1016
  continue;
988
1017
  }
@@ -1389,14 +1418,18 @@ export class NativeMacOSJobExecutor {
1389
1418
  }
1390
1419
  if (action.type === "read_file") {
1391
1420
  await reporter.progress(progressPercent, `Lendo ${action.path}`);
1392
- const fileContent = await this.readLocalFile(action.path, action.max_chars);
1393
- completionNotes.push(`Conteudo de ${action.path}:\n${fileContent}`);
1421
+ const fileContent = await this.readLocalFileSnapshot(action.path, action.max_chars);
1422
+ resultPayload.read_file = fileContent;
1423
+ resultPayload.summary = fileContent.summary;
1424
+ completionNotes.push(fileContent.summary);
1394
1425
  continue;
1395
1426
  }
1396
1427
  if (action.type === "list_files") {
1397
1428
  await reporter.progress(progressPercent, `Listando arquivos em ${action.path}`);
1398
- const listing = await this.listLocalFiles(action.path, action.limit);
1399
- completionNotes.push(`Arquivos em ${action.path}:\n${listing}`);
1429
+ const listing = await this.listLocalFilesSnapshot(action.path, action.limit);
1430
+ resultPayload.file_listing = listing;
1431
+ resultPayload.summary = listing.summary;
1432
+ completionNotes.push(listing.summary);
1400
1433
  continue;
1401
1434
  }
1402
1435
  if (action.type === "count_files") {
@@ -2364,7 +2397,7 @@ return appNames as text
2364
2397
  snapshot.summary = `A pasta ${targetPath} tem ${entries.length} item${entries.length === 1 ? "" : "s"} e ocupa ${formatBytesCompact(totalSize)}.${childPreview ? ` Itens visiveis agora: ${childPreview}.` : ""}`;
2365
2398
  return snapshot;
2366
2399
  }
2367
- const preview = includePreview ? await this.readLocalFile(resolved, 1200) : undefined;
2400
+ const preview = includePreview ? await this.readLocalFilePreview(resolved, 1200) : undefined;
2368
2401
  const snapshot = {
2369
2402
  captured_at: new Date().toISOString(),
2370
2403
  path: targetPath,
@@ -4961,9 +4994,9 @@ if let output = String(data: data, encoding: .utf8) {
4961
4994
  resized,
4962
4995
  };
4963
4996
  }
4964
- async readLocalFile(filePath, maxChars = 4000) {
4965
- const resolved = await this.resolveReadableFilePath(filePath);
4997
+ async loadReadableFileContent(resolved) {
4966
4998
  const extension = path.extname(resolved).toLowerCase();
4999
+ const mimeType = mimeTypeFromPath(resolved);
4967
5000
  if (TEXTUTIL_READABLE_EXTENSIONS.has(extension)) {
4968
5001
  const { stdout } = await this.runCommandCapture("textutil", [
4969
5002
  "-convert",
@@ -4971,17 +5004,76 @@ if let output = String(data: data, encoding: .utf8) {
4971
5004
  "-stdout",
4972
5005
  resolved,
4973
5006
  ]);
4974
- const content = sanitizeTextForJsonTransport(stdout);
4975
- return clipTextPreview(content || "(arquivo sem texto legivel)", maxChars);
5007
+ return {
5008
+ mimeType,
5009
+ encoding: "utf-8",
5010
+ isBinary: false,
5011
+ content: sanitizeTextForJsonTransport(stdout) || "(arquivo sem texto legivel)",
5012
+ };
4976
5013
  }
4977
5014
  const raw = await readFile(resolved);
4978
5015
  if (isLikelyBinaryBuffer(raw)) {
4979
5016
  const filename = path.basename(resolved);
4980
5017
  const detectedType = extension || "binario";
4981
- return clipText(`O arquivo ${filename} parece ser binario (${detectedType}) e nao pode ser lido como texto puro pelo Otto Bridge ainda.`, maxChars);
5018
+ return {
5019
+ mimeType,
5020
+ isBinary: true,
5021
+ binaryNotice: `O arquivo ${filename} parece ser binario (${detectedType}) e nao pode ser lido como texto puro pelo Otto Bridge ainda.`,
5022
+ };
5023
+ }
5024
+ return {
5025
+ mimeType,
5026
+ encoding: "utf-8",
5027
+ isBinary: false,
5028
+ content: sanitizeTextForJsonTransport(raw.toString("utf8")) || "(arquivo vazio)",
5029
+ };
5030
+ }
5031
+ async readLocalFilePreview(filePath, maxChars = 1200) {
5032
+ const resolved = await this.resolveReadableFilePath(filePath);
5033
+ const loaded = await this.loadReadableFileContent(resolved);
5034
+ if (loaded.isBinary) {
5035
+ return clipText(loaded.binaryNotice || "O arquivo parece ser binario e nao pode ser lido como texto puro.", maxChars);
5036
+ }
5037
+ return clipTextPreview(loaded.content || "(arquivo vazio)", maxChars);
5038
+ }
5039
+ async readLocalFileSnapshot(filePath, chunkSizeChars = 4000) {
5040
+ const resolved = await this.resolveReadableFilePath(filePath);
5041
+ const entryStat = await stat(resolved);
5042
+ const loaded = await this.loadReadableFileContent(resolved);
5043
+ const fileName = path.basename(resolved) || resolved;
5044
+ if (loaded.isBinary) {
5045
+ return {
5046
+ captured_at: new Date().toISOString(),
5047
+ path: filePath,
5048
+ resolved_path: resolved,
5049
+ name: fileName,
5050
+ mime_type: loaded.mimeType,
5051
+ size_bytes: entryStat.size,
5052
+ modified_at: entryStat.mtime.toISOString(),
5053
+ is_binary: true,
5054
+ binary_notice: loaded.binaryNotice,
5055
+ summary: loaded.binaryNotice || `O arquivo ${filePath} parece ser binario e nao pode ser lido como texto puro.`,
5056
+ };
4982
5057
  }
4983
- const content = sanitizeTextForJsonTransport(raw.toString("utf8"));
4984
- return clipTextPreview(content || "(arquivo vazio)", maxChars);
5058
+ const content = loaded.content || "(arquivo vazio)";
5059
+ const chunks = chunkTextForTransport(content, chunkSizeChars);
5060
+ return {
5061
+ captured_at: new Date().toISOString(),
5062
+ path: filePath,
5063
+ resolved_path: resolved,
5064
+ name: fileName,
5065
+ mime_type: loaded.mimeType,
5066
+ size_bytes: entryStat.size,
5067
+ modified_at: entryStat.mtime.toISOString(),
5068
+ is_binary: false,
5069
+ encoding: loaded.encoding || "utf-8",
5070
+ content: chunks.length <= 1 ? content : undefined,
5071
+ content_char_count: content.length,
5072
+ chunk_size_chars: Math.max(400, Math.min(Math.round(chunkSizeChars || 4000), 32_000)),
5073
+ chunk_count: chunks.length,
5074
+ chunks,
5075
+ summary: `Li ${filePath} por completo (${content.length} caracteres em ${chunks.length} bloco${chunks.length === 1 ? "" : "s"}).`,
5076
+ };
4985
5077
  }
4986
5078
  async resolveReadableFilePath(filePath) {
4987
5079
  const resolved = expandUserPath(filePath);
@@ -5052,22 +5144,55 @@ if let output = String(data: data, encoding: .utf8) {
5052
5144
  }
5053
5145
  return null;
5054
5146
  }
5055
- async listLocalFiles(directoryPath, limit = 40) {
5147
+ async listLocalFilesSnapshot(directoryPath, limit) {
5056
5148
  const resolved = expandUserPath(directoryPath);
5057
- const entries = await readdir(resolved, { withFileTypes: true });
5058
- const items = await Promise.all(entries.slice(0, limit).map(async (entry) => {
5149
+ const allEntries = await readdir(resolved, { withFileTypes: true });
5150
+ const sortedEntries = allEntries.sort((left, right) => {
5151
+ if (left.isDirectory() !== right.isDirectory()) {
5152
+ return left.isDirectory() ? -1 : 1;
5153
+ }
5154
+ return left.name.localeCompare(right.name);
5155
+ });
5156
+ const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) ? Math.max(1, Math.min(Math.round(limit), 5_000)) : null;
5157
+ const selectedEntries = effectiveLimit ? sortedEntries.slice(0, effectiveLimit) : sortedEntries;
5158
+ const items = await Promise.all(selectedEntries.map(async (entry) => {
5059
5159
  const entryPath = path.join(resolved, entry.name);
5060
- let suffix = "";
5160
+ let sizeBytes;
5161
+ let modifiedAt;
5061
5162
  try {
5062
5163
  const entryStat = await stat(entryPath);
5063
- suffix = entry.isDirectory() ? "/" : ` (${entryStat.size} bytes)`;
5164
+ if (!entry.isDirectory()) {
5165
+ sizeBytes = entryStat.size;
5166
+ }
5167
+ modifiedAt = entryStat.mtime.toISOString();
5064
5168
  }
5065
5169
  catch {
5066
- suffix = entry.isDirectory() ? "/" : "";
5170
+ // Ignore stat failures and return the visible entry metadata we have.
5067
5171
  }
5068
- return `${entry.name}${suffix}`;
5172
+ return {
5173
+ name: entry.name,
5174
+ path: entryPath,
5175
+ kind: entry.isDirectory() ? "directory" : (entry.isFile() ? "file" : "other"),
5176
+ size_bytes: sizeBytes,
5177
+ modified_at: modifiedAt,
5178
+ };
5069
5179
  }));
5070
- return items.length > 0 ? items.join("\n") : "(pasta vazia)";
5180
+ const summary = items.length === 0
5181
+ ? `A pasta ${directoryPath} esta vazia.`
5182
+ : effectiveLimit && allEntries.length > items.length
5183
+ ? `Listei ${items.length} itens visiveis em ${directoryPath} agora. A pasta tem ${allEntries.length} itens no total.`
5184
+ : `Listei ${items.length} itens de ${directoryPath} por completo.`;
5185
+ return {
5186
+ captured_at: new Date().toISOString(),
5187
+ path: directoryPath,
5188
+ resolved_path: resolved,
5189
+ name: path.basename(resolved) || resolved,
5190
+ item_count: items.length,
5191
+ total_item_count: allEntries.length,
5192
+ limit_applied: effectiveLimit || undefined,
5193
+ entries: items,
5194
+ summary,
5195
+ };
5071
5196
  }
5072
5197
  async countLocalFiles(directoryPath, extensions, recursive = true) {
5073
5198
  const resolved = expandUserPath(directoryPath);
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.9.0";
2
+ export const BRIDGE_VERSION = "0.9.1";
3
3
  export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
4
4
  export const DEFAULT_API_BASE_URL = "http://localhost:8000";
5
5
  export const DEFAULT_POLL_INTERVAL_MS = 3000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",