@leg3ndy/otto-bridge 1.0.12 → 1.1.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/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 o patch atual `1.1.1`, veja [`leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_1_PATCH.md`](../leg3ndy-ai-backend/docs/otto-bridge/releases/OTTO_BRIDGE_1_1_1_PATCH.md). Para o corte funcional da linha `1.1.0`, 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.1.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.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.
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.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`.
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.1` 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
@@ -141,7 +145,15 @@ Dentro do console, use:
141
145
 
142
146
  - `/model fast` para `OttoAI Fast`
143
147
  - `/model thinking` para `OttoAI Thinking`
148
+ - `/approval preview`, `/approval confirm` ou `/approval trusted` para trocar o modo de aprovação do device
144
149
  - `/status` para ver detalhes técnicos do bridge e do runtime
150
+ - `/workspace` ou `/workspace status` para ver o workspace ativo desta sessão
151
+ - `/workspace list` para listar workspaces anexados ao device
152
+ - `/workspace attach <path>` para anexar uma pasta/repo novo pelo helper autenticado
153
+ - `/workspace use <id|n>` para fixar um workspace anexado na sessão atual
154
+ - `/workspace clear` para limpar o binding atual do chat/sessão
155
+
156
+ No TTY, o composer agora fica ancorado no rodapé com placeholder `Peça algo ao Otto`, quebra por largura real do terminal e deixa o transcript do Otto sempre visível acima. Logo abaixo do input, o console mostra o modelo ativo, a barra de uso de contexto/tokens e o modo de aprovação atual; `Shift+Tab` alterna esse modo sem sair do console.
145
157
 
146
158
  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
159
 
@@ -157,7 +169,7 @@ Esse comando abre um shell local interativo para instalar extensoes, rodar coman
157
169
 
158
170
  ### WhatsApp Web em background
159
171
 
160
- Fluxo recomendado na linha `1.0.12`:
172
+ Fluxo recomendado na linha `1.1.1`:
161
173
 
162
174
  ```bash
163
175
  otto-bridge extensions --install whatsappweb
@@ -167,13 +179,13 @@ otto-bridge extensions --status whatsappweb
167
179
 
168
180
  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
181
 
170
- Contrato da linha `1.0.12`:
182
+ Contrato da linha `1.1.1`:
171
183
 
172
184
  - `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
173
185
  - `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
186
  - ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
175
187
 
176
- ## Handoff rapido da linha 1.0.12
188
+ ## Handoff rapido da linha 1.1.1
177
189
 
178
190
  Ja fechado no codigo:
179
191
 
@@ -187,8 +199,10 @@ Ja fechado no codigo:
187
199
  - o executor `native-macos` agora reporta o step atual por acao e publica inline artifacts estruturados para resultados locais nao-uploadados
188
200
  - 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
201
  - 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
202
+ - 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
203
  - filesystem e shell locais agora respeitam os roots declarados do workspace, em vez de operar como superficie global quando o job vier escopado
191
204
  - `workspace_policy` agora aplica perfis `observe_only`, `dev_assist`, `workspace_coding` e `release_operator` antes da execucao local de actions escopadas
205
+ - 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
206
  - `write_json_file`, `mkdir`, `move_file` e `delete_file` agora fazem parte da familia inicial de file ops tipadas do workspace
193
207
  - `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
208
  - `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 +221,10 @@ Ja fechado no codigo:
207
221
  - 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
222
  - `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
223
  - 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
224
+ - 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
225
+ - 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
226
+ - 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
227
+ - 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
228
  - 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
229
  - 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
230
  - 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
+ }