@leg3ndy/otto-bridge 0.9.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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 o patch atual `0.9.2`, com confirmacao bloqueante correta e exclusao segura via Lixeira, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_2_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_2_PATCH.md).
18
+ Para a release atual `1.0.1`, com runtime agentico formal, hub terminal e console alinhado ao Otto da web, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_1_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_1_PATCH.md).
19
19
 
20
20
  ## Distribuicao
21
21
 
@@ -29,6 +29,7 @@ O pacote ja esta estruturado para install via CLI:
29
29
 
30
30
  ```bash
31
31
  npm install -g @leg3ndy/otto-bridge
32
+ otto-bridge
32
33
  otto-bridge status
33
34
  otto-bridge version
34
35
  ```
@@ -37,14 +38,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
37
38
 
38
39
  ```bash
39
40
  npm pack
40
- npm install -g ./leg3ndy-otto-bridge-0.9.2.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.0.1.tgz
41
42
  ```
42
43
 
43
- Na linha `0.9.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.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
45
 
45
- No macOS, a linha `0.9.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.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
47
 
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.2` consolida esse caminho com confirmacao bloqueante correta, exclusao segura via Lixeira, planner LLM-first e leitura/listagem local sem truncamento silencioso.
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
49
 
49
50
  ## Publicacao
50
51
 
@@ -75,6 +76,23 @@ otto-bridge help
75
76
  otto-bridge --help
76
77
  ```
77
78
 
79
+ ### Abrir o hub interativo e ligar o runtime
80
+
81
+ ```bash
82
+ otto-bridge
83
+ otto-bridge home
84
+ ```
85
+
86
+ Em TTY, o comando sem argumentos agora abre o hub interativo com banner, setup, status, extensoes e o `Otto Console`. Se ja existir pairing salvo, o próprio `otto-bridge` sobe o runtime local automaticamente e mostra o estado da conexão no hub.
87
+
88
+ ### Setup interativo
89
+
90
+ ```bash
91
+ otto-bridge setup
92
+ ```
93
+
94
+ Esse fluxo pede `API base URL`, `pairing code`, nome do dispositivo e executor. Em install global interativo, o pacote tambem tenta abrir esse setup automaticamente quando ainda nao existe pairing salvo.
95
+
78
96
  ### Parear o dispositivo
79
97
 
80
98
  ```bash
@@ -92,29 +110,39 @@ Opcoes suportadas:
92
110
 
93
111
  No macOS, o caminho recomendado agora e o executor nativo do Otto Bridge. Se nenhum `--executor` for informado, o `pair` usa `native-macos` por padrao no Mac.
94
112
 
95
- ### Rodar o bridge
113
+ ### Runtime principal do bridge
96
114
 
97
115
  ```bash
98
- otto-bridge run
116
+ otto-bridge
99
117
  ```
100
118
 
101
- Se o executor estiver salvo no `config.json`, o `run` usa essa configuracao por padrao.
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.
102
120
 
103
- Para forcar o executor nativo no macOS sem reparar:
121
+ Se precisar do executor nativo no macOS sem reparar, atualize o pairing/config e reabra o hub:
104
122
 
105
123
  ```bash
106
- otto-bridge run --executor native-macos
124
+ otto-bridge setup
107
125
  ```
108
126
 
109
- O adapter `clawd-cursor` continua disponivel como override opcional:
127
+ `otto-bridge run` continua existindo apenas como alias legado/headless para compatibilidade operacional.
128
+
129
+ O adapter `clawd-cursor` continua disponivel como override opcional no pairing legado:
130
+
131
+ ```bash
132
+ otto-bridge pair --executor clawd-cursor --clawd-url http://127.0.0.1:3847
133
+ ```
134
+
135
+ ### Falar com o Otto no terminal
110
136
 
111
137
  ```bash
112
- otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
138
+ otto-bridge console
113
139
  ```
114
140
 
141
+ O console usa a mesma sessão local já ligada pelo `otto-bridge`, envia prompts naturais ao backend usando `device_token`, respeita quota/plano do usuário e só vira handoff local quando o pedido realmente tiver cara de ação no computador. Quando houver `device_job`, ele acompanha polling e resolve `confirm_required` no terminal.
142
+
115
143
  ### WhatsApp Web em background
116
144
 
117
- Fluxo recomendado na linha `0.9.2`:
145
+ Fluxo recomendado na linha `1.0.1`:
118
146
 
119
147
  ```bash
120
148
  otto-bridge extensions --install whatsappweb
@@ -124,13 +152,13 @@ otto-bridge extensions --status whatsappweb
124
152
 
125
153
  O setup agora abre o login do WhatsApp Web no helper/background browser do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
126
154
 
127
- Contrato da linha `0.9.2`:
155
+ Contrato da linha `1.0.1`:
128
156
 
129
157
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
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
131
- - ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
158
+ - `otto-bridge`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime do hub estiver ativo, sem depender de uma aba aberta no Safari
159
+ - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
132
160
 
133
- ## Handoff rapido da linha 0.9.2
161
+ ## Handoff rapido da linha 1.0.1
134
162
 
135
163
  Ja fechado no codigo:
136
164
 
@@ -139,11 +167,44 @@ Ja fechado no codigo:
139
167
  - resultado final dos `device_job` agora e persistido como contexto mais forte para o proximo turno do Otto
140
168
  - prompt bridge-aware no chat normal para ajudar o Otto a responder com base no que realmente aconteceu no device
141
169
  - runtime local agora publica `local_tools` para o Otto/backend saberem exatamente o que o device consegue fazer
170
+ - o runtime agentico agora executa `execution_graph` granular por acao, em vez de colapsar a corrida local inteira num unico `tool_call`
171
+ - o protocolo do runtime agora publica `graph_id`/`step_id` nos eventos de job e aceita controles por step para confirmacao/cancelamento
172
+ - o executor `native-macos` agora reporta o step atual por acao e publica inline artifacts estruturados para resultados locais nao-uploadados
173
+ - o `runtime_contract` agora pode declarar `workspace_context` com roots/targets e descriptors de `instruction_bundle`, `repo_manifest`, `workspace_index`, `workspace_memory` e `workspace_policy` para jobs de arquivo e shell
174
+ - o bridge agora resolve `workspace_context` em paths absolutos, detecta `repo_root`, carrega `AGENTS.md` por precedencia e publica `instruction_bundle`, `repo_manifest`, `workspace_index`, `workspace_memory` e `workspace_policy` no resultado
175
+ - filesystem e shell locais agora respeitam os roots declarados do workspace, em vez de operar como superficie global quando o job vier escopado
176
+ - `workspace_policy` agora aplica perfis `observe_only`, `dev_assist`, `workspace_coding` e `release_operator` antes da execucao local de actions escopadas
177
+ - `write_json_file`, `mkdir`, `move_file` e `delete_file` agora fazem parte da familia inicial de file ops tipadas do workspace
178
+ - `workspace.patch`, `git.clone`, `git.fetch`, `git.checkout`, `git.rebase`, `git.merge`, `git.tag`, `git.status`, `git.diff`, `git.stage`, `git.commit`, `git.push` e `workspace.tests` agora fazem parte da primeira familia tipada de coding agent do catalogo local
179
+ - `apply_patch`, `git_status`, `git_diff`, `run_tests`, `git_clone`, `git_fetch`, `git_checkout`, `git_rebase`, `git_merge`, `git_tag`, `git_add`, `git_commit` e `git_push` agora executam dentro do workspace escopado e devolvem snapshots estruturados em vez de shell cru
180
+ - `apply_patch` entende o formato `*** Begin Patch ... *** End Patch`, devolve `patch_set` estruturado e continua bloqueando paths fora do workspace permitido
181
+ - `run_tests` agora aceita `command` explicito ou `profile` tipado (`pytest`, `npm_test`, `pnpm_test`, `yarn_test`, `bun_test`, `lint`, `build`) e devolve `resolved_command` no snapshot final
182
+ - `git_clone`, `git_fetch`, `git_checkout`, `git_rebase`, `git_merge`, `git_tag`, `git_add`, `git_commit` e `git_push` sobem com `requires_confirmation`, devolvem receipts estruturados (`git_clone_receipt`, `git_fetch_receipt`, `git_checkout_receipt`, `git_rebase_receipt`, `git_merge_receipt`, `git_tag_receipt`, `git_stage_receipt`, `git_commit_receipt`, `git_push_receipt`) e tratam `nothing_to_commit` / `Everything up-to-date` como resultado observavel do job
183
+ - `repo_manifest` e `workspace_index` agora saem com `scoped_root_path`, manifests/lockfiles, key files e extensoes dominantes para grounding leve de repo
184
+ - `workspace_memory` agora reaproveita o historico recente do mesmo `workspace_id` e volta atualizada no resultado do job para grounding curto entre execucoes
185
+ - `workspace_memory` agora tambem respeita governanca persistida no backend, com `retention_days`, `retention_job_limit`, `last_cleared_at` e `last_cleared_reason`, que o modal do Otto Bridge pode ajustar sem sair do runtime
186
+ - automacoes locais do canal `bridge` agora disparam `/v1/devices/automations/local/bridge/trigger` em cada tick, reaproveitando o mesmo runtime agentico do chat com `device_job`, approvals e replay auditaveis
187
+ - o `runtime_contract` agora tambem pode declarar `worker_profiles`, e o executor ecoa esse bundle em `worker_profiles`/artifact para deixar explicito quais perfis de leitura, mudanca, validacao e release foram ativados no job
188
+ - o `execution_graph` agora pode marcar `worker_id` por step, e o `runtime_contract` tambem pode declarar `worker_execution` com `budget_seconds`, `timeout_seconds`, `retry_limit` e `max_parallel_tasks` por worker para preparar fan-out controlado
189
+ - o backend agora deriva `runtime_workers` e `runtime_handoffs` persistidos do mesmo graph, incluindo `worker_run_id`, `fanout_level/wave/group` e retry por worker a partir de `retry_of_worker_run_id` / `retry_attempt`
190
+ - `git_diff` agora carrega `stat` e `diff_excerpt`, e o backend usa isso para grounding mais estrito de respostas sobre diff longo
191
+ - o `runtime_contract` agora tambem declara `command_packs` formais (`plan-fast`, `plan-deep`, `instruction-updater`), e o bridge ecoa esses modos como artifact estruturado para deixar claro qual intensidade de planejamento/manutencao foi aplicada no job
192
+ - o `instruction_updater` agora e um descriptor explicito do runtime, com `source_entrypoints`, `target_paths` e `recommended_validation_stage_ids` quando o plano toca `AGENTS.md`, `README.md` ou `docs/`
193
+ - `workspace.tests` agora consegue resolver `profile=auto` via `validation_ladder`, executando stages como `typecheck`, `build`, `node_test`, `lint` e `pytest` por stack, com snapshot agregado por etapa
194
+ - o executor `native-macos` agora publica `runtime_hook_trace` somando hooks de lifecycle e tool-use (`session_start`, `pre_tool_use`, `post_tool_use`, `validation_ladder_started/completed`, `session_end`), preparando enforcement, replay fino e metricas futuras
195
+ - o bridge agora possui um CLI interativo proprio com setup inicial, hub terminal e `Otto Console` para conversar com o Otto pelo proprio terminal
196
+ - o backend agora expoe caminhos `device-auth` para o console do bridge usar chat com quota/plano (`/v1/devices/cli/chat/completions`) e acompanhar approval/job usando apenas `device_token`
142
197
  - o caminho principal do bridge usa `summary`/`narration_context` no lugar de resposta automatica pronta
143
198
  - `read_file` agora entrega conteudo completo segmentado em `content_chunks` para o Otto
144
199
  - `list_files` sem `limit` agora lista o diretorio inteiro, sem fallback silencioso para 40 itens
145
200
  - o planner do bridge prioriza o modelo para escolher a tool certa pelo contexto; regex fica como fallback
146
201
 
202
+ ### Testes locais do runtime
203
+
204
+ ```bash
205
+ npm run test:runtime
206
+ ```
207
+
147
208
  Ainda precisa reteste em campo:
148
209
 
149
210
  - fluxo completo do WhatsApp no helper `macos-helper`
@@ -0,0 +1,240 @@
1
+ function normalizePatchText(value) {
2
+ return value.replace(/\r\n/g, "\n");
3
+ }
4
+ function requirePatchPath(prefix, line) {
5
+ const targetPath = line.slice(prefix.length).trim();
6
+ if (!targetPath) {
7
+ throw new Error("O apply_patch recebeu uma operacao sem caminho de arquivo.");
8
+ }
9
+ return targetPath;
10
+ }
11
+ function parseUpdateHunks(lines) {
12
+ const hunks = [];
13
+ let current = null;
14
+ const ensureCurrentHunk = () => {
15
+ if (!current) {
16
+ current = { header: "@@", lines: [] };
17
+ hunks.push(current);
18
+ }
19
+ return current;
20
+ };
21
+ for (const line of lines) {
22
+ if (!line.trim()) {
23
+ continue;
24
+ }
25
+ if (line.startsWith("@@")) {
26
+ current = { header: line.trim(), lines: [] };
27
+ hunks.push(current);
28
+ continue;
29
+ }
30
+ if (line === "*** End of File") {
31
+ continue;
32
+ }
33
+ const prefix = line.slice(0, 1);
34
+ const hunk = ensureCurrentHunk();
35
+ if (prefix === " ") {
36
+ hunk.lines.push({ kind: "context", text: line.slice(1) });
37
+ continue;
38
+ }
39
+ if (prefix === "+") {
40
+ hunk.lines.push({ kind: "add", text: line.slice(1) });
41
+ continue;
42
+ }
43
+ if (prefix === "-") {
44
+ hunk.lines.push({ kind: "delete", text: line.slice(1) });
45
+ continue;
46
+ }
47
+ throw new Error(`Linha invalida no apply_patch: ${line}`);
48
+ }
49
+ return hunks.filter((item) => item.lines.length > 0);
50
+ }
51
+ export function parseStructuredPatch(patchText) {
52
+ const normalized = normalizePatchText(String(patchText || ""));
53
+ const lines = normalized.split("\n");
54
+ const operations = [];
55
+ let index = 0;
56
+ while (index < lines.length && !lines[index]?.trim()) {
57
+ index += 1;
58
+ }
59
+ if (lines[index]?.trim() !== "*** Begin Patch") {
60
+ throw new Error("O apply_patch exige o formato *** Begin Patch ... *** End Patch.");
61
+ }
62
+ index += 1;
63
+ while (index < lines.length) {
64
+ const line = lines[index]?.trimEnd() || "";
65
+ if (!line.trim()) {
66
+ index += 1;
67
+ continue;
68
+ }
69
+ if (line.trim() === "*** End Patch") {
70
+ return {
71
+ operations,
72
+ patch_char_count: normalized.length,
73
+ };
74
+ }
75
+ if (line.startsWith("*** Add File: ")) {
76
+ const targetPath = requirePatchPath("*** Add File: ", line);
77
+ index += 1;
78
+ const contentLines = [];
79
+ while (index < lines.length) {
80
+ const currentLine = lines[index] || "";
81
+ if (currentLine.startsWith("*** ")) {
82
+ break;
83
+ }
84
+ if (!currentLine.startsWith("+")) {
85
+ throw new Error(`Linha invalida em Add File: ${currentLine}`);
86
+ }
87
+ contentLines.push(currentLine.slice(1));
88
+ index += 1;
89
+ }
90
+ operations.push({
91
+ type: "add",
92
+ path: targetPath,
93
+ content_lines: contentLines,
94
+ });
95
+ continue;
96
+ }
97
+ if (line.startsWith("*** Delete File: ")) {
98
+ operations.push({
99
+ type: "delete",
100
+ path: requirePatchPath("*** Delete File: ", line),
101
+ });
102
+ index += 1;
103
+ continue;
104
+ }
105
+ if (line.startsWith("*** Update File: ")) {
106
+ const targetPath = requirePatchPath("*** Update File: ", line);
107
+ index += 1;
108
+ let moveTo;
109
+ if ((lines[index] || "").startsWith("*** Move to: ")) {
110
+ moveTo = requirePatchPath("*** Move to: ", lines[index] || "");
111
+ index += 1;
112
+ }
113
+ const updateLines = [];
114
+ while (index < lines.length) {
115
+ const currentLine = lines[index] || "";
116
+ if (currentLine.startsWith("*** Add File: ")
117
+ || currentLine.startsWith("*** Delete File: ")
118
+ || currentLine.startsWith("*** Update File: ")
119
+ || currentLine.trim() === "*** End Patch") {
120
+ break;
121
+ }
122
+ updateLines.push(currentLine);
123
+ index += 1;
124
+ }
125
+ operations.push({
126
+ type: "update",
127
+ path: targetPath,
128
+ move_to: moveTo,
129
+ hunks: parseUpdateHunks(updateLines),
130
+ });
131
+ continue;
132
+ }
133
+ throw new Error(`Nao reconheci a operacao do apply_patch: ${line}`);
134
+ }
135
+ throw new Error("O apply_patch nao encontrou o marcador *** End Patch.");
136
+ }
137
+ export function collectStructuredPatchTargets(patchText) {
138
+ try {
139
+ const parsed = parseStructuredPatch(patchText);
140
+ const targets = [];
141
+ const seen = new Set();
142
+ for (const operation of parsed.operations) {
143
+ const pushTarget = (targetPath, accessMode) => {
144
+ const key = `${accessMode}:${targetPath}`;
145
+ if (!targetPath || seen.has(key)) {
146
+ return;
147
+ }
148
+ seen.add(key);
149
+ targets.push({ path: targetPath, accessMode });
150
+ };
151
+ if (operation.type === "add") {
152
+ pushTarget(operation.path, "write");
153
+ continue;
154
+ }
155
+ if (operation.type === "delete") {
156
+ pushTarget(operation.path, "delete");
157
+ continue;
158
+ }
159
+ pushTarget(operation.path, "write");
160
+ if (operation.move_to) {
161
+ pushTarget(operation.move_to, "write");
162
+ }
163
+ }
164
+ return targets;
165
+ }
166
+ catch {
167
+ return [];
168
+ }
169
+ }
170
+ export function structuredPatchHasDelete(patchText) {
171
+ return collectStructuredPatchTargets(patchText).some((item) => item.accessMode === "delete");
172
+ }
173
+ function splitTextForPatch(value) {
174
+ const normalized = normalizePatchText(value);
175
+ if (!normalized) {
176
+ return { lines: [], trailingNewline: false };
177
+ }
178
+ const trailingNewline = normalized.endsWith("\n");
179
+ const lines = normalized.split("\n");
180
+ if (trailingNewline) {
181
+ lines.pop();
182
+ }
183
+ return { lines, trailingNewline };
184
+ }
185
+ function joinPatchedText(lines, trailingNewline) {
186
+ if (lines.length === 0) {
187
+ return "";
188
+ }
189
+ return `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
190
+ }
191
+ function findMatchingSegment(lines, targetLines, startIndex) {
192
+ if (targetLines.length === 0) {
193
+ return startIndex;
194
+ }
195
+ for (let index = startIndex; index <= lines.length - targetLines.length; index += 1) {
196
+ let matches = true;
197
+ for (let offset = 0; offset < targetLines.length; offset += 1) {
198
+ if (lines[index + offset] !== targetLines[offset]) {
199
+ matches = false;
200
+ break;
201
+ }
202
+ }
203
+ if (matches) {
204
+ return index;
205
+ }
206
+ }
207
+ if (startIndex > 0) {
208
+ return findMatchingSegment(lines, targetLines, 0);
209
+ }
210
+ return -1;
211
+ }
212
+ export function applyStructuredUpdateToText(originalText, operation) {
213
+ const { lines, trailingNewline } = splitTextForPatch(originalText);
214
+ let cursor = 0;
215
+ let addedLineCount = 0;
216
+ let deletedLineCount = 0;
217
+ for (const hunk of operation.hunks) {
218
+ const oldLines = hunk.lines
219
+ .filter((line) => line.kind !== "add")
220
+ .map((line) => line.text);
221
+ const newLines = hunk.lines
222
+ .filter((line) => line.kind !== "delete")
223
+ .map((line) => line.text);
224
+ const matchIndex = oldLines.length === 0
225
+ ? cursor
226
+ : findMatchingSegment(lines, oldLines, cursor);
227
+ if (matchIndex < 0) {
228
+ throw new Error(`Nao consegui localizar o contexto do patch para ${operation.path}.`);
229
+ }
230
+ lines.splice(matchIndex, oldLines.length, ...newLines);
231
+ cursor = matchIndex + newLines.length;
232
+ addedLineCount += hunk.lines.filter((line) => line.kind === "add").length;
233
+ deletedLineCount += hunk.lines.filter((line) => line.kind === "delete").length;
234
+ }
235
+ return {
236
+ text: joinPatchedText(lines, trailingNewline),
237
+ addedLineCount,
238
+ deletedLineCount,
239
+ };
240
+ }