@leg3ndy/otto-bridge 1.0.11 → 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 +23 -8
- package/dist/agentic_runtime/workspace/manager.js +9 -5
- package/dist/attached_workspaces.js +259 -0
- package/dist/cli_terminal.js +184 -82
- package/dist/executors/native_macos.js +67 -0
- package/dist/http.js +6 -0
- package/dist/main.js +4 -0
- package/dist/runtime.js +32 -0
- package/dist/tool_catalog.js +8 -0
- package/dist/types.js +2 -2
- package/package.json +1 -1
- package/scripts/postinstall.mjs +1 -1
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
|
|
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.
|
|
41
|
+
npm install -g ./leg3ndy-otto-bridge-1.1.0.tgz
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Na linha `1.0
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/dist/cli_terminal.js
CHANGED
|
@@ -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,9 +53,9 @@ 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
|
-
const CONSOLE_COMPOSER_CURSOR_COLUMN =
|
|
58
|
+
const CONSOLE_COMPOSER_CURSOR_COLUMN = 2;
|
|
58
59
|
class CliRuntimeSession {
|
|
59
60
|
config;
|
|
60
61
|
runtime = null;
|
|
@@ -674,27 +675,139 @@ function printConsoleScreen(runtimeSession, modelMode) {
|
|
|
674
675
|
printSoft(`Comandos: ${CONSOLE_COMMAND_HINT}`);
|
|
675
676
|
console.log("");
|
|
676
677
|
}
|
|
677
|
-
function
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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;
|
|
686
691
|
}
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
+
});
|
|
692
733
|
}
|
|
693
|
-
function
|
|
694
|
-
|
|
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;
|
|
695
808
|
}
|
|
696
|
-
function
|
|
697
|
-
|
|
809
|
+
function renderPromptFrameLine(width, edgeLeft, edgeRight) {
|
|
810
|
+
return style(`${edgeLeft}${"─".repeat(width)}${edgeRight}`, ANSI.brandBlue, supportsAnsi());
|
|
698
811
|
}
|
|
699
812
|
function sliceByWidth(text, width) {
|
|
700
813
|
if (width <= 0) {
|
|
@@ -773,6 +886,9 @@ function renderConsoleComposerLines(value, innerWidth, enabled) {
|
|
|
773
886
|
cursorColumn: layout.cursorColumn,
|
|
774
887
|
};
|
|
775
888
|
}
|
|
889
|
+
function renderConsoleComposerText(lines) {
|
|
890
|
+
return lines.join("\r\n");
|
|
891
|
+
}
|
|
776
892
|
export function tryConsumeControlSequence(buffer) {
|
|
777
893
|
const knownNewlineSequences = [
|
|
778
894
|
"\u001b[13;2u",
|
|
@@ -829,102 +945,75 @@ async function askConsoleInput(rl) {
|
|
|
829
945
|
const enabled = supportsAnsi();
|
|
830
946
|
const availableWidth = Number(output.columns || 96);
|
|
831
947
|
const innerWidth = Math.max(42, Math.min(availableWidth - 8, 116));
|
|
948
|
+
const sectionTopOffsetFromInputLine = 1;
|
|
832
949
|
let renderedOnce = false;
|
|
833
|
-
let lastRenderedLineCount = 0;
|
|
834
|
-
let lastCursorLineIndex = 0;
|
|
835
950
|
let value = "";
|
|
836
|
-
let pendingControlBuffer = "";
|
|
837
951
|
const cleanup = () => {
|
|
838
952
|
input.removeListener("data", onData);
|
|
839
|
-
disableTerminalEnhancedKeys();
|
|
840
953
|
input.setRawMode(false);
|
|
841
954
|
input.pause();
|
|
842
955
|
rl.resume();
|
|
843
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
|
+
};
|
|
844
971
|
const render = () => {
|
|
845
|
-
const composer = renderConsoleComposerLines(value, innerWidth, enabled);
|
|
846
972
|
if (renderedOnce) {
|
|
847
|
-
moveCursor(output, 0, -(1 + lastCursorLineIndex));
|
|
848
973
|
cursorTo(output, 0);
|
|
974
|
+
moveCursor(output, 0, -sectionTopOffsetFromInputLine);
|
|
849
975
|
}
|
|
850
976
|
else {
|
|
851
977
|
renderedOnce = true;
|
|
852
978
|
}
|
|
853
979
|
const top = renderPromptFrameLine(innerWidth + 2, "┌", "┐");
|
|
854
980
|
const border = style("│", ANSI.brandBlue, enabled);
|
|
981
|
+
const middle = `${border} ${renderInputContent()} ${border}`;
|
|
855
982
|
const bottom = renderPromptFrameLine(innerWidth + 2, "└", "┘");
|
|
856
|
-
|
|
857
|
-
output.write(
|
|
858
|
-
output
|
|
859
|
-
moveCursor(output, 0, -
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
lastCursorLineIndex = composer.cursorLineIndex;
|
|
863
|
-
};
|
|
864
|
-
const insertNewline = () => {
|
|
865
|
-
value = `${value}\n`;
|
|
866
|
-
render();
|
|
867
|
-
};
|
|
868
|
-
const submitPrompt = () => {
|
|
869
|
-
if (renderedOnce) {
|
|
870
|
-
moveCursor(output, 0, lastRenderedLineCount - lastCursorLineIndex + 1);
|
|
871
|
-
cursorTo(output, 0);
|
|
872
|
-
}
|
|
873
|
-
cleanup();
|
|
874
|
-
output.write("\n");
|
|
875
|
-
resolve(normalizeText(value));
|
|
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);
|
|
876
989
|
};
|
|
877
990
|
const onData = (chunk) => {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (
|
|
991
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
992
|
+
for (const char of Array.from(text)) {
|
|
993
|
+
if (char === "\u0003") {
|
|
881
994
|
cleanup();
|
|
882
995
|
reject(createCliExitError());
|
|
883
996
|
return;
|
|
884
997
|
}
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
else {
|
|
890
|
-
pendingControlBuffer = pendingControlBuffer.slice(1);
|
|
891
|
-
}
|
|
892
|
-
submitPrompt();
|
|
998
|
+
if (char === "\r" || char === "\n") {
|
|
999
|
+
cleanup();
|
|
1000
|
+
output.write("\n");
|
|
1001
|
+
resolve(normalizeText(value));
|
|
893
1002
|
return;
|
|
894
1003
|
}
|
|
895
|
-
if (
|
|
896
|
-
pendingControlBuffer = pendingControlBuffer.slice(1);
|
|
897
|
-
insertNewline();
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
if (pendingControlBuffer.startsWith("\u007f") || pendingControlBuffer.startsWith("\b")) {
|
|
901
|
-
pendingControlBuffer = pendingControlBuffer.slice(1);
|
|
1004
|
+
if (char === "\u007f" || char === "\b") {
|
|
902
1005
|
value = value.slice(0, -1);
|
|
903
1006
|
render();
|
|
904
1007
|
continue;
|
|
905
1008
|
}
|
|
906
|
-
|
|
907
|
-
if (controlSequence) {
|
|
908
|
-
if (controlSequence.action === "incomplete") {
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
pendingControlBuffer = pendingControlBuffer.slice(controlSequence.consumed);
|
|
912
|
-
if (controlSequence.action === "newline") {
|
|
913
|
-
insertNewline();
|
|
914
|
-
}
|
|
1009
|
+
if (char === "\u001b") {
|
|
915
1010
|
continue;
|
|
916
1011
|
}
|
|
917
|
-
const [char] = Array.from(pendingControlBuffer);
|
|
918
|
-
if (!char) {
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
pendingControlBuffer = pendingControlBuffer.slice(char.length);
|
|
922
1012
|
value += char;
|
|
923
1013
|
render();
|
|
924
1014
|
}
|
|
925
1015
|
};
|
|
926
1016
|
render();
|
|
927
|
-
enableTerminalEnhancedKeys();
|
|
928
1017
|
input.on("data", onData);
|
|
929
1018
|
});
|
|
930
1019
|
}
|
|
@@ -1231,9 +1320,10 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
|
|
|
1231
1320
|
const conversation = [];
|
|
1232
1321
|
const printConsoleHelp = () => {
|
|
1233
1322
|
printSection("Console");
|
|
1234
|
-
printSoft("Comandos: /help, /model [fast|thinking], /status, /clear, /exit");
|
|
1323
|
+
printSoft("Comandos: /help, /model [fast|thinking], /workspace, /status, /clear, /exit");
|
|
1235
1324
|
printSoft("Bridge: otto-bridge terminal, otto-bridge extensions --install <name>, otto-bridge update");
|
|
1236
|
-
printSoft("Composer: Enter envia
|
|
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");
|
|
1237
1327
|
};
|
|
1238
1328
|
const handlePrompt = async (promptText) => {
|
|
1239
1329
|
const normalizedPrompt = normalizeText(promptText);
|
|
@@ -1279,6 +1369,18 @@ async function runOttoConsole(rl, config, runtimeSession, options) {
|
|
|
1279
1369
|
}
|
|
1280
1370
|
return;
|
|
1281
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
|
+
}
|
|
1282
1384
|
if (normalizedPrompt === "/exit") {
|
|
1283
1385
|
throw createCliExitError();
|
|
1284
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
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
|
}
|
package/dist/tool_catalog.js
CHANGED
|
@@ -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
|
|
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 =
|
|
13
|
+
export const BRIDGE_LOCAL_TOOLS_VERSION = 8;
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -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
|
|
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"], {
|