@leg3ndy/otto-bridge 1.0.12 → 1.1.0

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.12`, com composer TTY simplificado para um prompt multiline sem moldura, cursor alinhado ao placeholder e newline sem abrir caixas duplicadas, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_0_12_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_0_12_PATCH.md).
18
+ Para a release atual `1.1.0`, que consolida o baseline TTY single-line e inaugura a rail de coding com `Commit`, `Workspace` e `Snapshots`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_0_RELEASE.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.12.tgz
41
+ npm install -g ./leg3ndy-otto-bridge-1.1.0.tgz
42
42
  ```
43
43
 
44
- Na linha `1.0.12`, `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.1.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.
45
45
 
46
- No macOS, a linha `1.0.12` 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.1.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`.
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.12` preserva esse fluxo, simplifica o prompt TTY para evitar artefatos de moldura em terminais mais temperamentais, mantem o composer multiline e melhora as views de status/extensoes e o refresh ao sair do `Terminal`.
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.1.0` preserva o prompt TTY single-line como baseline estavel e adiciona a camada workspace-first com rail de coding, trust/policy por workspace, source control first-class, working set persistido e grounding remoto por repositório.
49
49
 
50
50
  ## Publicacao
51
51
 
@@ -84,6 +84,8 @@ otto-bridge
84
84
 
85
85
  Em TTY, o comando sem argumentos agora abre o hub interativo com banner, setup, status, extensoes e o `Otto Console`. Se ja existir pairing salvo, o próprio `otto-bridge` sobe o runtime local automaticamente e mostra o estado da conexão no hub.
86
86
 
87
+ Na web, o acesso ao Otto Bridge fica no botao com icone de estrela/brilho ao lado do botao de anexos.
88
+
87
89
  ### Setup interativo
88
90
 
89
91
  ```bash
@@ -92,6 +94,8 @@ otto-bridge setup
92
94
 
93
95
  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.
94
96
 
97
+ O `pairing code` e gerado no modal do Otto Bridge aberto pela interface web, no botao com icone de estrela ao lado de anexos.
98
+
95
99
  ### Parear o dispositivo
96
100
 
97
101
  ```bash
@@ -142,6 +146,11 @@ Dentro do console, use:
142
146
  - `/model fast` para `OttoAI Fast`
143
147
  - `/model thinking` para `OttoAI Thinking`
144
148
  - `/status` para ver detalhes técnicos do bridge e do runtime
149
+ - `/workspace` ou `/workspace status` para ver o workspace ativo desta sessão
150
+ - `/workspace list` para listar workspaces anexados ao device
151
+ - `/workspace attach <path>` para anexar uma pasta/repo novo pelo helper autenticado
152
+ - `/workspace use <id|n>` para fixar um workspace anexado na sessão atual
153
+ - `/workspace clear` para limpar o binding atual do chat/sessão
145
154
 
146
155
  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
156
 
@@ -157,7 +166,7 @@ Esse comando abre um shell local interativo para instalar extensoes, rodar coman
157
166
 
158
167
  ### WhatsApp Web em background
159
168
 
160
- Fluxo recomendado na linha `1.0.12`:
169
+ Fluxo recomendado na linha `1.1.0`:
161
170
 
162
171
  ```bash
163
172
  otto-bridge extensions --install whatsappweb
@@ -167,13 +176,13 @@ otto-bridge extensions --status whatsappweb
167
176
 
168
177
  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
178
 
170
- Contrato da linha `1.0.12`:
179
+ Contrato da linha `1.1.0`:
171
180
 
172
181
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
173
182
  - `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
183
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
175
184
 
176
- ## Handoff rapido da linha 1.0.12
185
+ ## Handoff rapido da linha 1.1.0
177
186
 
178
187
  Ja fechado no codigo:
179
188
 
@@ -187,8 +196,10 @@ Ja fechado no codigo:
187
196
  - o executor `native-macos` agora reporta o step atual por acao e publica inline artifacts estruturados para resultados locais nao-uploadados
188
197
  - 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
189
198
  - 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
199
+ - workspaces anexados pelo modal web agora tambem viram parte do metadata do helper: o `otto-bridge` consulta `/v1/devices/runtime/workspaces` com `device_token`, sonda localmente `repo_root`, `branch` e a cadeia de `AGENTS.md` antes do `device.hello`, e reage a `device.metadata.refresh` para republicar esse snapshot quando o usuario anexa/remove um workspace com o bridge online
190
200
  - filesystem e shell locais agora respeitam os roots declarados do workspace, em vez de operar como superficie global quando o job vier escopado
191
201
  - `workspace_policy` agora aplica perfis `observe_only`, `dev_assist`, `workspace_coding` e `release_operator` antes da execucao local de actions escopadas
202
+ - cada workspace anexado agora tambem persiste o proprio `policy_profile_id`; o modal do Otto Bridge permite trocar esse perfil, o banner do chat mostra o trust atual e o runtime trata esse valor como teto real da sessao antes de shell, patch, delete ou release
192
203
  - `write_json_file`, `mkdir`, `move_file` e `delete_file` agora fazem parte da familia inicial de file ops tipadas do workspace
193
204
  - `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
194
205
  - `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
@@ -207,6 +218,10 @@ Ja fechado no codigo:
207
218
  - 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/`
208
219
  - `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
209
220
  - 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
221
+ - o frontend agora consome `runtime_hook_trace`, `runtime_replay` e o progresso corrente como feed inline persistente no chat, para o usuario ver o Otto lendo, procurando, editando, validando e concluindo em tempo real
222
+ - o chat web agora mostra esse plano/checklist acima do input enquanto o job local roda, e deixa o bloco detalhado de runtime apenas para o resultado final quando houver detalhe real de arquivo/diff/source control/validacao/erro
223
+ - o catalogo local agora inclui `filesystem.open_path`, usado para abrir um arquivo/pasta local no app padrao do macOS ou revelar no Finder a partir dos links clicaveis do timeline
224
+ - o modal do Otto Bridge agora mostra `Jobs locais` em resumo textual, `Snapshot do workspace` com source control/validacao/working set e links de path que disparam `open_path` local, sem repetir a timeline detalhada do chat
210
225
  - o bridge agora possui um CLI interativo proprio com setup inicial, hub terminal e `Otto Console` para conversar com o Otto pelo proprio terminal
211
226
  - 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`
212
227
  - o caminho principal do bridge usa `summary`/`narration_context` no lugar de resposta automatica pronta
@@ -245,6 +245,12 @@ function normalizeWorkspacePolicy(value, workspaceId, actionTypes, destructiveAc
245
245
  const title = asString(value?.title) || WORKSPACE_POLICY_TITLES[profileId] || "Workspace Policy";
246
246
  const summary = asString(value?.summary)
247
247
  || `Policy ${profileId} carregada para o workspace ${workspaceId} com ${allowedActionTypes.length} action types permitidos.`;
248
+ const rationale = asString(value?.rationale)
249
+ || (normalizedActionTypes.length > 0
250
+ ? value?.profile_id
251
+ ? `Perfil ${profileId} aplicado ao workspace para actions do runtime: ${normalizedActionTypes.join(", ")}.`
252
+ : `Perfil inferido a partir das actions do runtime: ${normalizedActionTypes.join(", ")}.`
253
+ : undefined);
248
254
  return {
249
255
  profile_id: profileId || "observe_only",
250
256
  title,
@@ -252,9 +258,7 @@ function normalizeWorkspacePolicy(value, workspaceId, actionTypes, destructiveAc
252
258
  allowed_action_types: allowedActionTypes,
253
259
  blocked_action_types: blockedActionTypes,
254
260
  requires_confirmation_action_types: requiresConfirmationActionTypes,
255
- rationale: asString(value?.rationale) || (normalizedActionTypes.length > 0
256
- ? `Perfil inferido a partir das actions do runtime: ${normalizedActionTypes.join(", ")}.`
257
- : undefined),
261
+ rationale,
258
262
  allow_shell: allowShell,
259
263
  allow_code_write: allowCodeWrite,
260
264
  allow_destructive: allowDestructive,
@@ -1036,9 +1040,9 @@ export function assertActionAllowedByWorkspacePolicy(workspace, actionType) {
1036
1040
  return;
1037
1041
  }
1038
1042
  if (workspacePolicy.blocked_action_types.includes(normalizedActionType)) {
1039
- throw new Error(`A action ${normalizedActionType} foi bloqueada pela policy ${workspacePolicy.profile_id} do workspace ${workspace.workspaceId}.`);
1043
+ throw new Error(`A policy ${workspacePolicy.profile_id} deste workspace bloqueia a action ${normalizedActionType}. Troque o perfil de confiança do workspace para liberar essa operação.`);
1040
1044
  }
1041
1045
  if (!workspacePolicy.allowed_action_types.includes(normalizedActionType)) {
1042
- throw new Error(`A action ${normalizedActionType} nao esta permitida pela policy ${workspacePolicy.profile_id} do workspace ${workspace.workspaceId}.`);
1046
+ throw new Error(`A action ${normalizedActionType} nao está permitida no perfil ${workspacePolicy.profile_id} deste workspace. Ajuste a policy do workspace para permitir essa operação.`);
1043
1047
  }
1044
1048
  }
@@ -0,0 +1,259 @@
1
+ import { execFile } from "node:child_process";
2
+ import { stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { deleteDeviceJson, getDeviceJson, postDeviceJson } from "./http.js";
6
+ import { resolveWorkspaceContext, expandUserPathLike } from "./agentic_runtime/workspace/manager.js";
7
+ const execFileAsync = promisify(execFile);
8
+ const DEFAULT_IGNORED_DIRECTORY_NAMES = [
9
+ ".git",
10
+ ".next",
11
+ ".venv",
12
+ "__pycache__",
13
+ "build",
14
+ "coverage",
15
+ "dist",
16
+ "node_modules",
17
+ "venv",
18
+ ];
19
+ const DEFAULT_KEY_FILE_PATTERNS = [
20
+ "AGENTS.md",
21
+ "README.md",
22
+ "package.json",
23
+ "pyproject.toml",
24
+ "requirements.txt",
25
+ "Cargo.toml",
26
+ "go.mod",
27
+ "pnpm-lock.yaml",
28
+ "package-lock.json",
29
+ "yarn.lock",
30
+ ];
31
+ function asString(value) {
32
+ return typeof value === "string" ? value.trim() : "";
33
+ }
34
+ function uniqueStrings(values) {
35
+ return Array.from(new Set(values.map((item) => asString(item)).filter(Boolean)));
36
+ }
37
+ function slugifyWorkspaceToken(value) {
38
+ const slug = value
39
+ .toLowerCase()
40
+ .replace(/[^a-z0-9]+/g, "_")
41
+ .replace(/^_+|_+$/g, "");
42
+ return slug.slice(0, 120) || "workspace_local";
43
+ }
44
+ function deriveWorkspaceLabel(rootPath) {
45
+ const normalized = rootPath.replace(/[\\/]+$/, "");
46
+ const basename = path.basename(normalized);
47
+ return basename || normalized || "Workspace local";
48
+ }
49
+ function normalizeAttachedWorkspaceCandidate(value) {
50
+ if (!value || typeof value !== "object") {
51
+ return null;
52
+ }
53
+ const payload = value;
54
+ const rootPath = asString(payload.root_path)
55
+ || asString(payload.path)
56
+ || asString(payload.workspace_root);
57
+ if (!rootPath) {
58
+ return null;
59
+ }
60
+ const workspaceId = asString(payload.workspace_id) || `workspace_${slugifyWorkspaceToken(rootPath)}`;
61
+ return {
62
+ workspace_id: workspaceId,
63
+ label: asString(payload.label) || asString(payload.name) || deriveWorkspaceLabel(rootPath),
64
+ root_path: rootPath,
65
+ repo_root: asString(payload.repo_root) || rootPath,
66
+ branch: asString(payload.branch) || null,
67
+ is_git_repo: Boolean(payload.is_git_repo),
68
+ agents_loaded: payload.agents_loaded === true,
69
+ agents_paths: Array.isArray(payload.agents_paths)
70
+ ? uniqueStrings(payload.agents_paths)
71
+ : [],
72
+ policy_profile_id: asString(payload.policy_profile_id) || asString(payload.workspace_policy_profile) || "workspace_coding",
73
+ policy_updated_at: asString(payload.policy_updated_at) || null,
74
+ updated_at: asString(payload.updated_at) || null,
75
+ };
76
+ }
77
+ async function resolveWorkspaceRootPath(rootPath, baseCwd) {
78
+ const expanded = expandUserPathLike(rootPath, baseCwd);
79
+ try {
80
+ const fileStat = await stat(expanded);
81
+ if (fileStat.isFile()) {
82
+ return path.dirname(expanded);
83
+ }
84
+ }
85
+ catch {
86
+ return expanded;
87
+ }
88
+ return expanded;
89
+ }
90
+ async function readGitBranch(repoRoot) {
91
+ try {
92
+ const { stdout } = await execFileAsync("git", ["-C", repoRoot, "branch", "--show-current"], {
93
+ timeout: 4000,
94
+ });
95
+ const branch = asString(stdout);
96
+ return branch || null;
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ export async function probeAttachedWorkspace(workspace, options) {
103
+ const resolvedRootPath = await resolveWorkspaceRootPath(workspace.root_path, options?.baseCwd);
104
+ const resolvedWorkspace = await resolveWorkspaceContext({
105
+ baseCwd: options?.baseCwd,
106
+ workspaceContext: {
107
+ workspace_id: workspace.workspace_id,
108
+ roots: [
109
+ {
110
+ root_id: "workspace_root_01",
111
+ path: resolvedRootPath,
112
+ kind: "declared_path",
113
+ scope_reason: "attached_workspace_root",
114
+ action_types: ["filesystem_inspect"],
115
+ source: "bridge_attached_workspace_probe",
116
+ },
117
+ ],
118
+ targets: [
119
+ {
120
+ target_id: "workspace_target_01",
121
+ path: resolvedRootPath,
122
+ kind: "directory",
123
+ access_mode: "enumerate",
124
+ action_type: "filesystem_inspect",
125
+ source: "bridge_attached_workspace_probe",
126
+ },
127
+ ],
128
+ instruction_bundle: {
129
+ resolver: "bridge_local_agents_md",
130
+ entrypoints: ["AGENTS.md"],
131
+ precedence: [
132
+ "nearest_target_agents_md",
133
+ "workspace_repo_agents_md",
134
+ "monorepo_root_agents_md",
135
+ ],
136
+ },
137
+ repo_manifest: {
138
+ resolver: "bridge_workspace_repo_probe",
139
+ marker_paths: [".git"],
140
+ manifest_paths: ["AGENTS.md", "README.md", "package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod"],
141
+ lockfile_paths: ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb", "poetry.lock", "Pipfile.lock", "Cargo.lock", "go.sum"],
142
+ include_instruction_sources: true,
143
+ vcs_hint: "git",
144
+ },
145
+ workspace_index: {
146
+ resolver: "bridge_workspace_scan_light",
147
+ max_depth: 1,
148
+ max_directories: 8,
149
+ max_files: 32,
150
+ ignored_directory_names: DEFAULT_IGNORED_DIRECTORY_NAMES,
151
+ key_file_patterns: DEFAULT_KEY_FILE_PATTERNS,
152
+ },
153
+ },
154
+ });
155
+ const repoRoot = resolvedWorkspace?.repoRoot || resolvedRootPath;
156
+ const branch = resolvedWorkspace?.repoRoot
157
+ ? await readGitBranch(resolvedWorkspace.repoRoot).catch(() => null)
158
+ : null;
159
+ const agentsPaths = resolvedWorkspace?.instructionBundle?.sources.map((item) => item.path) || [];
160
+ return {
161
+ workspace_id: workspace.workspace_id,
162
+ label: workspace.label || deriveWorkspaceLabel(resolvedRootPath),
163
+ root_path: resolvedRootPath,
164
+ repo_root: repoRoot,
165
+ branch: branch || workspace.branch || null,
166
+ is_git_repo: Boolean(resolvedWorkspace?.repoRoot),
167
+ agents_loaded: agentsPaths.length > 0,
168
+ agents_paths: agentsPaths,
169
+ policy_profile_id: workspace.policy_profile_id || "workspace_coding",
170
+ policy_updated_at: workspace.policy_updated_at || workspace.updated_at || null,
171
+ updated_at: new Date().toISOString(),
172
+ };
173
+ }
174
+ export async function loadAttachedWorkspacesForHello(options) {
175
+ const fallbackCandidates = Array.isArray(options.fallbackMetadata?.attached_workspaces)
176
+ ? options.fallbackMetadata?.attached_workspaces || []
177
+ : [];
178
+ let rawCandidates = fallbackCandidates;
179
+ try {
180
+ const response = await getDeviceJson(options.apiBaseUrl, options.deviceToken, "/v1/devices/runtime/workspaces");
181
+ if (Array.isArray(response.workspaces)) {
182
+ rawCandidates = response.workspaces;
183
+ }
184
+ }
185
+ catch {
186
+ // Fall back to whatever the local config already knows.
187
+ }
188
+ const candidates = rawCandidates
189
+ .map((item) => normalizeAttachedWorkspaceCandidate(item))
190
+ .filter((item) => Boolean(item));
191
+ if (candidates.length === 0) {
192
+ return [];
193
+ }
194
+ const probed = await Promise.all(candidates.map(async (workspace) => {
195
+ try {
196
+ return await probeAttachedWorkspace(workspace, { baseCwd: options.baseCwd });
197
+ }
198
+ catch {
199
+ return {
200
+ ...workspace,
201
+ root_path: expandUserPathLike(workspace.root_path, options.baseCwd),
202
+ updated_at: new Date().toISOString(),
203
+ };
204
+ }
205
+ }));
206
+ return probed;
207
+ }
208
+ export async function listAttachedWorkspacesForRuntime(options) {
209
+ return await loadAttachedWorkspacesForHello({
210
+ apiBaseUrl: options.apiBaseUrl,
211
+ deviceToken: options.deviceToken,
212
+ baseCwd: options.baseCwd,
213
+ });
214
+ }
215
+ export async function getRuntimeSessionWorkspace(options) {
216
+ const sessionId = asString(options.sessionId);
217
+ if (!sessionId) {
218
+ return null;
219
+ }
220
+ const response = await getDeviceJson(options.apiBaseUrl, options.deviceToken, `/v1/devices/runtime/sessions/${encodeURIComponent(sessionId)}/workspace`);
221
+ const workspace = normalizeAttachedWorkspaceCandidate(response.workspace);
222
+ if (!workspace) {
223
+ return null;
224
+ }
225
+ return await probeAttachedWorkspace(workspace, { baseCwd: options.baseCwd });
226
+ }
227
+ export async function attachWorkspaceForRuntime(options) {
228
+ const response = await postDeviceJson(options.apiBaseUrl, options.deviceToken, "/v1/devices/runtime/workspaces/attach", {
229
+ root_path: options.rootPath,
230
+ label: asString(options.label) || undefined,
231
+ repo_root: asString(options.repoRoot) || undefined,
232
+ branch: asString(options.branch) || undefined,
233
+ });
234
+ const workspace = normalizeAttachedWorkspaceCandidate(response.workspace);
235
+ if (!workspace) {
236
+ throw new Error("Workspace anexado sem payload válido.");
237
+ }
238
+ return await probeAttachedWorkspace(workspace, { baseCwd: options.baseCwd });
239
+ }
240
+ export async function activateWorkspaceForRuntime(options) {
241
+ const workspaceId = asString(options.workspaceId);
242
+ const sessionId = asString(options.sessionId);
243
+ if (!workspaceId || !sessionId) {
244
+ throw new Error("Workspace e sessão são obrigatórios para ativar o contexto do console.");
245
+ }
246
+ const response = await postDeviceJson(options.apiBaseUrl, options.deviceToken, `/v1/devices/runtime/workspaces/${encodeURIComponent(workspaceId)}/activate`, { session_id: sessionId });
247
+ const workspace = normalizeAttachedWorkspaceCandidate(response.workspace);
248
+ if (!workspace) {
249
+ return null;
250
+ }
251
+ return await probeAttachedWorkspace(workspace, { baseCwd: options.baseCwd });
252
+ }
253
+ export async function clearRuntimeWorkspaceForSession(options) {
254
+ const sessionId = asString(options.sessionId);
255
+ if (!sessionId) {
256
+ return;
257
+ }
258
+ await deleteDeviceJson(options.apiBaseUrl, options.deviceToken, `/v1/devices/runtime/sessions/${encodeURIComponent(sessionId)}/workspace`);
259
+ }
@@ -2,9 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { createInterface } from "node:readline/promises";
5
- import { cursorTo, moveCursor, } from "node:readline";
5
+ import { clearScreenDown, cursorTo, moveCursor, } from "node:readline";
6
6
  import process, { stdin as input, stdout as output } from "node:process";
7
7
  import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, normalizeInstalledExtensions, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
8
+ import { activateWorkspaceForRuntime, attachWorkspaceForRuntime, clearRuntimeWorkspaceForSession, getRuntimeSessionWorkspace, listAttachedWorkspacesForRuntime, } from "./attached_workspaces.js";
8
9
  import { streamDeviceCliChat, } from "./chat_cli_client.js";
9
10
  import { formatManagedBridgeExtensionStatus, isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
10
11
  import { pairDevice } from "./pairing.js";
@@ -52,7 +53,7 @@ const MAX_RENDERED_LIST_ENTRIES_COMPACT = 10;
52
53
  const MAX_RENDERED_FILE_CHARS = 6_000;
53
54
  const MAX_RENDERED_FILE_CHARS_COMPACT = 1_400;
54
55
  const CONSOLE_PLACEHOLDER = "Peça algo ao Otto";
55
- const CONSOLE_COMMAND_HINT = "/help, /model [fast|thinking], /status, /clear, /exit";
56
+ const CONSOLE_COMMAND_HINT = "/help, /model [fast|thinking], /workspace, /status, /clear, /exit";
56
57
  const CONSOLE_COMPOSER_PROMPT_WIDTH = 2;
57
58
  const CONSOLE_COMPOSER_CURSOR_COLUMN = 2;
58
59
  class CliRuntimeSession {
@@ -674,11 +675,139 @@ function printConsoleScreen(runtimeSession, modelMode) {
674
675
  printSoft(`Comandos: ${CONSOLE_COMMAND_HINT}`);
675
676
  console.log("");
676
677
  }
677
- function enableTerminalEnhancedKeys() {
678
- output.write("\u001b[>4;2m");
678
+ function formatConsoleWorkspaceLine(workspace, options) {
679
+ const prefix = options?.isActive ? "*" : "-";
680
+ const indexPrefix = typeof options?.index === "number" ? `${options.index + 1}. ` : "";
681
+ const label = normalizeText(workspace.label || workspace.workspace_id || "Workspace local");
682
+ const rootPath = normalizeText(workspace.repo_root || workspace.root_path || "");
683
+ const branch = normalizeText(workspace.branch);
684
+ const agents = workspace.agents_loaded ? "AGENTS.md" : "sem AGENTS.md";
685
+ return `${prefix} ${indexPrefix}${label}${rootPath ? ` -> ${rootPath}` : ""}${branch ? ` [${branch}]` : ""} (${agents})`;
686
+ }
687
+ function resolveConsoleWorkspaceSelection(token, workspaces) {
688
+ const normalized = normalizeText(token);
689
+ if (!normalized) {
690
+ return null;
691
+ }
692
+ const numericIndex = Number(normalized);
693
+ if (Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= workspaces.length) {
694
+ return workspaces[numericIndex - 1] || null;
695
+ }
696
+ const lowered = normalized.toLowerCase();
697
+ return workspaces.find((workspace) => {
698
+ const workspaceId = normalizeText(workspace.workspace_id).toLowerCase();
699
+ const label = normalizeText(workspace.label).toLowerCase();
700
+ const rootPath = normalizeText(workspace.root_path || workspace.repo_root).toLowerCase();
701
+ return workspaceId === lowered || label === lowered || rootPath === lowered;
702
+ }) || null;
703
+ }
704
+ async function printConsoleWorkspaceStatus(config, sessionId) {
705
+ const [workspaces, activeWorkspace] = await Promise.all([
706
+ listAttachedWorkspacesForRuntime({
707
+ apiBaseUrl: config.apiBaseUrl,
708
+ deviceToken: config.deviceToken,
709
+ baseCwd: process.cwd(),
710
+ }),
711
+ getRuntimeSessionWorkspace({
712
+ apiBaseUrl: config.apiBaseUrl,
713
+ deviceToken: config.deviceToken,
714
+ sessionId,
715
+ baseCwd: process.cwd(),
716
+ }),
717
+ ]);
718
+ printSection("Workspace");
719
+ if (activeWorkspace) {
720
+ printSuccess(`Ativo neste console: ${formatConsoleWorkspaceLine(activeWorkspace, { isActive: true })}`);
721
+ }
722
+ else {
723
+ printMuted("Nenhum workspace ativo neste console.");
724
+ }
725
+ if (workspaces.length === 0) {
726
+ printSoft("Nenhum workspace anexado. Use /workspace attach <caminho>.");
727
+ return;
728
+ }
729
+ workspaces.forEach((workspace, index) => {
730
+ const isActive = normalizeText(workspace.workspace_id) === normalizeText(activeWorkspace?.workspace_id);
731
+ console.log(formatConsoleWorkspaceLine(workspace, { index, isActive }));
732
+ });
733
+ }
734
+ async function handleConsoleWorkspaceCommand(normalizedPrompt, config, sessionId) {
735
+ if (!normalizedPrompt.startsWith("/workspace")) {
736
+ return false;
737
+ }
738
+ const remainder = normalizeText(normalizedPrompt.slice("/workspace".length));
739
+ if (!remainder || remainder === "list" || remainder === "status") {
740
+ await printConsoleWorkspaceStatus(config, sessionId);
741
+ return true;
742
+ }
743
+ if (remainder === "help") {
744
+ printSection("Workspace");
745
+ printSoft("/workspace lista os workspaces anexados e o ativo");
746
+ printSoft("/workspace attach <path> anexa uma pasta/repo neste device");
747
+ printSoft("/workspace use <id|n> ativa o workspace para o console atual");
748
+ printSoft("/workspace clear remove o workspace ativo deste console");
749
+ return true;
750
+ }
751
+ if (remainder.startsWith("attach ")) {
752
+ const rootPath = normalizeText(remainder.slice("attach ".length));
753
+ if (!rootPath) {
754
+ printWarning("Use /workspace attach <caminho>.");
755
+ return true;
756
+ }
757
+ const workspace = await attachWorkspaceForRuntime({
758
+ apiBaseUrl: config.apiBaseUrl,
759
+ deviceToken: config.deviceToken,
760
+ rootPath,
761
+ baseCwd: process.cwd(),
762
+ });
763
+ printSuccess(`Workspace anexado: ${formatConsoleWorkspaceLine(workspace, { isActive: false })}`);
764
+ return true;
765
+ }
766
+ if (remainder.startsWith("use ")) {
767
+ const selectionToken = normalizeText(remainder.slice("use ".length));
768
+ if (!selectionToken) {
769
+ printWarning("Use /workspace use <id|numero>.");
770
+ return true;
771
+ }
772
+ const workspaces = await listAttachedWorkspacesForRuntime({
773
+ apiBaseUrl: config.apiBaseUrl,
774
+ deviceToken: config.deviceToken,
775
+ baseCwd: process.cwd(),
776
+ });
777
+ const selectedWorkspace = resolveConsoleWorkspaceSelection(selectionToken, workspaces);
778
+ if (!selectedWorkspace?.workspace_id) {
779
+ printWarning("Workspace não encontrado. Rode /workspace para listar as opções.");
780
+ return true;
781
+ }
782
+ const activated = await activateWorkspaceForRuntime({
783
+ apiBaseUrl: config.apiBaseUrl,
784
+ deviceToken: config.deviceToken,
785
+ workspaceId: selectedWorkspace.workspace_id,
786
+ sessionId,
787
+ baseCwd: process.cwd(),
788
+ });
789
+ if (activated) {
790
+ printSuccess(`Workspace ativo neste console: ${formatConsoleWorkspaceLine(activated, { isActive: true })}`);
791
+ }
792
+ else {
793
+ printWarning("Não foi possível ativar o workspace selecionado.");
794
+ }
795
+ return true;
796
+ }
797
+ if (remainder === "clear") {
798
+ await clearRuntimeWorkspaceForSession({
799
+ apiBaseUrl: config.apiBaseUrl,
800
+ deviceToken: config.deviceToken,
801
+ sessionId,
802
+ });
803
+ printMuted("Workspace ativo deste console removido.");
804
+ return true;
805
+ }
806
+ printWarning("Comando de workspace inválido. Use /workspace help.");
807
+ return true;
679
808
  }
680
- function disableTerminalEnhancedKeys() {
681
- output.write("\u001b[>4m");
809
+ function renderPromptFrameLine(width, edgeLeft, edgeRight) {
810
+ return style(`${edgeLeft}${"".repeat(width)}${edgeRight}`, ANSI.brandBlue, supportsAnsi());
682
811
  }
683
812
  function sliceByWidth(text, width) {
684
813
  if (width <= 0) {
@@ -757,6 +886,9 @@ function renderConsoleComposerLines(value, innerWidth, enabled) {
757
886
  cursorColumn: layout.cursorColumn,
758
887
  };
759
888
  }
889
+ function renderConsoleComposerText(lines) {
890
+ return lines.join("\r\n");
891
+ }
760
892
  export function tryConsumeControlSequence(buffer) {
761
893
  const knownNewlineSequences = [
762
894
  "\u001b[13;2u",
@@ -813,98 +945,75 @@ async function askConsoleInput(rl) {
813
945
  const enabled = supportsAnsi();
814
946
  const availableWidth = Number(output.columns || 96);
815
947
  const innerWidth = Math.max(42, Math.min(availableWidth - 8, 116));
948
+ const sectionTopOffsetFromInputLine = 1;
816
949
  let renderedOnce = false;
817
- let lastRenderedLineCount = 0;
818
- let lastCursorLineIndex = 0;
819
950
  let value = "";
820
- let pendingControlBuffer = "";
821
951
  const cleanup = () => {
822
952
  input.removeListener("data", onData);
823
- disableTerminalEnhancedKeys();
824
953
  input.setRawMode(false);
825
954
  input.pause();
826
955
  rl.resume();
827
956
  };
957
+ const renderInputContent = () => {
958
+ const promptPlain = "> ";
959
+ const promptStyled = style(">", `${ANSI.bold}${ANSI.white}`, enabled);
960
+ const maxValueLength = Math.max(0, innerWidth - promptPlain.length);
961
+ if (!value) {
962
+ const placeholder = truncate(CONSOLE_PLACEHOLDER, maxValueLength);
963
+ const padded = `${style(placeholder, ANSI.slate, enabled)}${" ".repeat(Math.max(0, maxValueLength - placeholder.length))}`;
964
+ return `${promptStyled} ${padded}`;
965
+ }
966
+ const visibleValue = value.length > maxValueLength
967
+ ? value.slice(value.length - maxValueLength)
968
+ : value;
969
+ return `${promptStyled} ${visibleValue}${" ".repeat(Math.max(0, maxValueLength - visibleValue.length))}`;
970
+ };
828
971
  const render = () => {
829
- const composer = renderConsoleComposerLines(value, innerWidth, enabled);
830
972
  if (renderedOnce) {
831
- moveCursor(output, 0, -lastCursorLineIndex);
832
973
  cursorTo(output, 0);
974
+ moveCursor(output, 0, -sectionTopOffsetFromInputLine);
833
975
  }
834
976
  else {
835
977
  renderedOnce = true;
836
978
  }
837
- output.write("\u001b[J");
838
- output.write(composer.renderedLines.join("\n"));
839
- moveCursor(output, 0, -Math.max(0, composer.renderedLines.length - 1 - composer.cursorLineIndex));
840
- cursorTo(output, CONSOLE_COMPOSER_CURSOR_COLUMN + composer.cursorColumn);
841
- lastRenderedLineCount = composer.renderedLines.length;
842
- lastCursorLineIndex = composer.cursorLineIndex;
843
- };
844
- const insertNewline = () => {
845
- value = `${value}\n`;
846
- render();
847
- };
848
- const submitPrompt = () => {
849
- if (renderedOnce) {
850
- moveCursor(output, 0, Math.max(0, lastRenderedLineCount - 1 - lastCursorLineIndex));
851
- cursorTo(output, 0);
852
- }
853
- cleanup();
854
- output.write("\n");
855
- resolve(normalizeText(value));
979
+ const top = renderPromptFrameLine(innerWidth + 2, "", "┐");
980
+ const border = style("", ANSI.brandBlue, enabled);
981
+ const middle = `${border} ${renderInputContent()} ${border}`;
982
+ const bottom = renderPromptFrameLine(innerWidth + 2, "└", "┘");
983
+ clearScreenDown(output);
984
+ output.write(`${top}\n${middle}\n${bottom}\n`);
985
+ cursorTo(output, 0);
986
+ moveCursor(output, 0, -2);
987
+ const visibleValueLength = Math.min(value.length, Math.max(0, innerWidth - 2));
988
+ cursorTo(output, 4 + visibleValueLength);
856
989
  };
857
990
  const onData = (chunk) => {
858
- pendingControlBuffer += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
859
- while (pendingControlBuffer.length > 0) {
860
- if (pendingControlBuffer.startsWith("\u0003")) {
991
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
992
+ for (const char of Array.from(text)) {
993
+ if (char === "\u0003") {
861
994
  cleanup();
862
995
  reject(createCliExitError());
863
996
  return;
864
997
  }
865
- if (pendingControlBuffer.startsWith("\r")) {
866
- if (pendingControlBuffer.startsWith("\r\n")) {
867
- pendingControlBuffer = pendingControlBuffer.slice(2);
868
- }
869
- else {
870
- pendingControlBuffer = pendingControlBuffer.slice(1);
871
- }
872
- submitPrompt();
998
+ if (char === "\r" || char === "\n") {
999
+ cleanup();
1000
+ output.write("\n");
1001
+ resolve(normalizeText(value));
873
1002
  return;
874
1003
  }
875
- if (pendingControlBuffer.startsWith("\n")) {
876
- pendingControlBuffer = pendingControlBuffer.slice(1);
877
- insertNewline();
878
- continue;
879
- }
880
- if (pendingControlBuffer.startsWith("\u007f") || pendingControlBuffer.startsWith("\b")) {
881
- pendingControlBuffer = pendingControlBuffer.slice(1);
1004
+ if (char === "\u007f" || char === "\b") {
882
1005
  value = value.slice(0, -1);
883
1006
  render();
884
1007
  continue;
885
1008
  }
886
- const controlSequence = tryConsumeControlSequence(pendingControlBuffer);
887
- if (controlSequence) {
888
- if (controlSequence.action === "incomplete") {
889
- return;
890
- }
891
- pendingControlBuffer = pendingControlBuffer.slice(controlSequence.consumed);
892
- if (controlSequence.action === "newline") {
893
- insertNewline();
894
- }
1009
+ if (char === "\u001b") {
895
1010
  continue;
896
1011
  }
897
- const [char] = Array.from(pendingControlBuffer);
898
- if (!char) {
899
- return;
900
- }
901
- pendingControlBuffer = pendingControlBuffer.slice(char.length);
902
1012
  value += char;
903
1013
  render();
904
1014
  }
905
1015
  };
906
1016
  render();
907
- enableTerminalEnhancedKeys();
908
1017
  input.on("data", onData);
909
1018
  });
910
1019
  }
@@ -1211,9 +1320,10 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1211
1320
  const conversation = [];
1212
1321
  const printConsoleHelp = () => {
1213
1322
  printSection("Console");
1214
- printSoft("Comandos: /help, /model [fast|thinking], /status, /clear, /exit");
1323
+ printSoft("Comandos: /help, /model [fast|thinking], /workspace, /status, /clear, /exit");
1215
1324
  printSoft("Bridge: otto-bridge terminal, otto-bridge extensions --install <name>, otto-bridge update");
1216
- printSoft("Composer: Enter envia; Shift+Enter quebra linha quando suportado; Ctrl+J tambem insere newline.");
1325
+ printSoft("Composer: Enter envia. Multiline desativado para evitar duplicacao no terminal.");
1326
+ printSoft("Workspace: /workspace list, /workspace attach <path>, /workspace use <id|n>, /workspace clear");
1217
1327
  };
1218
1328
  const handlePrompt = async (promptText) => {
1219
1329
  const normalizedPrompt = normalizeText(promptText);
@@ -1259,6 +1369,18 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
1259
1369
  }
1260
1370
  return;
1261
1371
  }
1372
+ if (normalizedPrompt.startsWith("/workspace")) {
1373
+ try {
1374
+ if (await handleConsoleWorkspaceCommand(normalizedPrompt, config, sessionId)) {
1375
+ return;
1376
+ }
1377
+ }
1378
+ catch (error) {
1379
+ const detail = error instanceof Error ? error.message : String(error || "Erro ao gerenciar workspace.");
1380
+ printError(detail);
1381
+ return;
1382
+ }
1383
+ }
1262
1384
  if (normalizedPrompt === "/exit") {
1263
1385
  throw createCliExitError();
1264
1386
  }
@@ -941,6 +941,17 @@ function parseStructuredActions(job) {
941
941
  }
942
942
  continue;
943
943
  }
944
+ if (type === "open_path" || type === "open_local_path" || type === "reveal_in_finder") {
945
+ const targetPath = asString(action.path) || asString(action.target_path) || asString(action.file_path);
946
+ if (targetPath) {
947
+ actions.push({
948
+ type: "open_path",
949
+ path: targetPath,
950
+ reveal_in_finder: action.reveal_in_finder === true || type === "reveal_in_finder",
951
+ });
952
+ }
953
+ continue;
954
+ }
944
955
  if (type === "create_note" || type === "write_note") {
945
956
  const text = asString(action.text) || asString(action.body) || asString(action.content);
946
957
  if (text) {
@@ -2062,6 +2073,26 @@ export class NativeMacOSJobExecutor {
2062
2073
  });
2063
2074
  continue;
2064
2075
  }
2076
+ if (action.type === "open_path") {
2077
+ await reportActionProgress(action.reveal_in_finder === true
2078
+ ? `Revelando ${action.path} no Finder`
2079
+ : `Abrindo ${action.path} no app padrão`);
2080
+ const openedPath = await this.openPathSnapshot(action.path, action.reveal_in_finder === true, workspaceContext);
2081
+ resultPayload.open_path = openedPath;
2082
+ resultPayload.summary = openedPath.summary;
2083
+ appendActionArtifact("open_path_result", {
2084
+ summary: openedPath.summary,
2085
+ path: openedPath.resolved_path,
2086
+ filename: openedPath.name,
2087
+ });
2088
+ completionNotes.push(openedPath.summary);
2089
+ appendHookEvent("post_tool_use", {
2090
+ stepId: currentActionStepId,
2091
+ message: `${action.type} concluido.`,
2092
+ metadata: { action_type: action.type, status: "completed" },
2093
+ });
2094
+ continue;
2095
+ }
2065
2096
  if (action.type === "read_file") {
2066
2097
  await reportActionProgress(`Lendo ${action.path}`);
2067
2098
  const fileContent = await this.readLocalFileSnapshot(action.path, action.max_chars, workspaceContext);
@@ -3044,6 +3075,37 @@ export class NativeMacOSJobExecutor {
3044
3075
  }
3045
3076
  await this.runCommand("open", [url]);
3046
3077
  }
3078
+ async openPathSnapshot(targetPath, revealInFinder = false, workspaceContext) {
3079
+ const resolvedPath = workspaceContext
3080
+ ? assertPathInsideWorkspace(workspaceContext, targetPath)
3081
+ : expandUserPath(targetPath);
3082
+ const entryStat = await stat(resolvedPath);
3083
+ const itemKind = entryStat.isDirectory()
3084
+ ? "directory"
3085
+ : entryStat.isFile()
3086
+ ? "file"
3087
+ : "other";
3088
+ if (revealInFinder === true) {
3089
+ await this.runCommand("open", ["-R", resolvedPath]);
3090
+ }
3091
+ else {
3092
+ await this.runCommand("open", [resolvedPath]);
3093
+ }
3094
+ const summary = revealInFinder
3095
+ ? `Revelei ${resolvedPath} no Finder do macOS.`
3096
+ : itemKind === "directory"
3097
+ ? `Abri a pasta ${resolvedPath} no Finder do macOS.`
3098
+ : `Abri ${resolvedPath} no app padrao do macOS.`;
3099
+ return {
3100
+ captured_at: new Date().toISOString(),
3101
+ path: targetPath,
3102
+ resolved_path: resolvedPath,
3103
+ name: path.basename(resolvedPath) || resolvedPath,
3104
+ item_kind: itemKind,
3105
+ reveal_in_finder: revealInFinder,
3106
+ summary,
3107
+ };
3108
+ }
3047
3109
  async ensureSafariSpotifySearchUrl(url) {
3048
3110
  const targetUrl = normalizeUrl(url);
3049
3111
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -8398,6 +8460,11 @@ if let output = String(data: data, encoding: .utf8) {
8398
8460
  if (action.type === "filesystem_inspect") {
8399
8461
  return `Inspecao local concluida em ${action.path}`;
8400
8462
  }
8463
+ if (action.type === "open_path") {
8464
+ return action.reveal_in_finder
8465
+ ? `${action.path} foi revelado no Finder`
8466
+ : `${action.path} foi aberto no app padrao do macOS`;
8467
+ }
8401
8468
  if (action.type === "read_file") {
8402
8469
  return `${action.path} foi lido no macOS`;
8403
8470
  }
package/dist/http.js CHANGED
@@ -60,6 +60,12 @@ export async function postDeviceJson(apiBaseUrl, deviceToken, pathname, body) {
60
60
  body: JSON.stringify(body),
61
61
  });
62
62
  }
63
+ export async function deleteDeviceJson(apiBaseUrl, deviceToken, pathname) {
64
+ return await requestJson(apiBaseUrl, pathname, {
65
+ method: "DELETE",
66
+ headers: buildDeviceAuthHeaders(deviceToken),
67
+ });
68
+ }
63
69
  export async function uploadDeviceJobArtifact(apiBaseUrl, deviceToken, jobId, params) {
64
70
  const form = new FormData();
65
71
  form.append("file", new Blob([Buffer.from(params.bytes)], { type: params.contentType || "application/octet-stream" }), params.filename);
package/dist/main.js CHANGED
@@ -122,6 +122,10 @@ Console:
122
122
  /help
123
123
  /model fast
124
124
  /model thinking
125
+ /workspace
126
+ /workspace attach <path>
127
+ /workspace use <id|n>
128
+ /workspace clear
125
129
  /status
126
130
  /clear
127
131
  /exit
package/dist/runtime.js CHANGED
@@ -7,6 +7,7 @@ import { isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "
7
7
  import { LocalAutomationRuntime } from "./local_automations.js";
8
8
  import { buildLocalToolCatalog } from "./tool_catalog.js";
9
9
  import { parseJobRuntimeManifest, runtimeStepIdForEvent, } from "./runtime_contract.js";
10
+ import { loadAttachedWorkspacesForHello } from "./attached_workspaces.js";
10
11
  function delay(ms) {
11
12
  return new Promise((resolve) => setTimeout(resolve, ms));
12
13
  }
@@ -108,6 +109,19 @@ export class BridgeRuntime {
108
109
  emit(event) {
109
110
  this.options.logger?.event?.(event);
110
111
  }
112
+ async refreshHelloMetadata(socket, reason = "manual") {
113
+ if (socket.readyState !== WebSocket.OPEN) {
114
+ return;
115
+ }
116
+ try {
117
+ await this.sendHello(socket);
118
+ this.logInfo(`[otto-bridge] hello metadata refreshed reason=${reason}`);
119
+ }
120
+ catch (error) {
121
+ const detail = error instanceof Error ? error.message : String(error);
122
+ this.logError(`[otto-bridge] hello metadata refresh failed: ${detail}`);
123
+ }
124
+ }
111
125
  async buildHelloMetadata() {
112
126
  const metadata = {
113
127
  ...(this.config.metadata || {}),
@@ -164,6 +178,21 @@ export class BridgeRuntime {
164
178
  });
165
179
  metadata.local_tools_version = toolCatalog.version;
166
180
  metadata.local_tools = toolCatalog.tools;
181
+ const attachedWorkspaces = await loadAttachedWorkspacesForHello({
182
+ apiBaseUrl: this.config.apiBaseUrl,
183
+ deviceToken: this.config.deviceToken,
184
+ fallbackMetadata: this.config.metadata || {},
185
+ }).catch((error) => {
186
+ const detail = error instanceof Error ? error.message : String(error);
187
+ this.logWarn(`[otto-bridge] attached workspace probe failed: ${detail}`);
188
+ return [];
189
+ });
190
+ if (attachedWorkspaces.length > 0) {
191
+ metadata.attached_workspaces = attachedWorkspaces;
192
+ }
193
+ else {
194
+ delete metadata.attached_workspaces;
195
+ }
167
196
  return metadata;
168
197
  }
169
198
  async sendHello(socket) {
@@ -367,6 +396,9 @@ export class BridgeRuntime {
367
396
  case "device.job.cancel":
368
397
  await this.cancelJob(String(message.job_id || ""), String(message.step_id || ""));
369
398
  return;
399
+ case "device.metadata.refresh":
400
+ await this.refreshHelloMetadata(socket, String(message.reason || "remote_request"));
401
+ return;
370
402
  default:
371
403
  this.logInfo(`[otto-bridge] event=${type || "unknown"} payload=${JSON.stringify(message)}`);
372
404
  }
@@ -65,6 +65,14 @@ const NATIVE_MACOS_BASE_TOOLS = [
65
65
  mode: "observe",
66
66
  action_types: ["read_file"],
67
67
  },
68
+ {
69
+ id: "filesystem.open_path",
70
+ title: "Abrir arquivo ou pasta local",
71
+ description: "Abre um caminho local no app padrao do macOS para inspecao rapida.",
72
+ category: "filesystem",
73
+ mode: "control",
74
+ action_types: ["open_path"],
75
+ },
68
76
  {
69
77
  id: "filesystem.write_text",
70
78
  title: "Escrita de arquivo de texto",
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "1.0.12";
2
+ export const BRIDGE_VERSION = "1.1.0";
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;
@@ -10,4 +10,4 @@ export const DEFAULT_RECONNECT_MAX_DELAY_MS = 15000;
10
10
  export const DEFAULT_EXECUTOR_TYPE = "mock";
11
11
  export const DEFAULT_CLAWD_CURSOR_BASE_URL = "http://127.0.0.1:3847";
12
12
  export const DEFAULT_CLAWD_CURSOR_POLL_INTERVAL_MS = 1500;
13
- export const BRIDGE_LOCAL_TOOLS_VERSION = 7;
13
+ export const BRIDGE_LOCAL_TOOLS_VERSION = 8;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "1.0.12",
3
+ "version": "1.1.0",
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.12");
27
+ console.log("\n[otto-bridge] Welcome to OTTOAI 1.1.0");
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"], {