@leg3ndy/otto-bridge 0.9.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -17
- package/dist/agentic_runtime/patch/structured_patch.js +240 -0
- package/dist/agentic_runtime/workspace/manager.js +1044 -0
- package/dist/chat_cli_client.js +91 -0
- package/dist/cli_terminal.js +668 -0
- package/dist/executors/native_macos.js +2778 -115
- package/dist/local_automations.js +33 -11
- package/dist/main.js +25 -3
- package/dist/runtime.js +136 -32
- package/dist/runtime_cli_client.js +18 -0
- package/dist/runtime_contract.js +516 -0
- package/dist/tool_catalog.js +148 -1
- package/dist/types.js +2 -2
- package/package.json +7 -2
- package/scripts/postinstall.mjs +35 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { defaultDeviceName, getBridgeConfigPath, loadBridgeConfig, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
|
|
5
|
+
import { streamDeviceCliChat, } from "./chat_cli_client.js";
|
|
6
|
+
import { formatManagedBridgeExtensionStatus, isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
|
|
7
|
+
import { pairDevice } from "./pairing.js";
|
|
8
|
+
import { BridgeRuntime, } from "./runtime.js";
|
|
9
|
+
import { cancelRuntimeCliJob, confirmRuntimeCliJob, getRuntimeCliJob, } from "./runtime_cli_client.js";
|
|
10
|
+
import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_API_BASE_URL, } from "./types.js";
|
|
11
|
+
const ANSI = {
|
|
12
|
+
reset: "\u001b[0m",
|
|
13
|
+
dim: "\u001b[2m",
|
|
14
|
+
bold: "\u001b[1m",
|
|
15
|
+
coral: "\u001b[38;5;216m",
|
|
16
|
+
blue: "\u001b[38;5;111m",
|
|
17
|
+
teal: "\u001b[38;5;80m",
|
|
18
|
+
amber: "\u001b[38;5;221m",
|
|
19
|
+
red: "\u001b[38;5;203m",
|
|
20
|
+
green: "\u001b[38;5;114m",
|
|
21
|
+
white: "\u001b[38;5;255m",
|
|
22
|
+
};
|
|
23
|
+
const OTTOAI_BANNER = [
|
|
24
|
+
" ██████╗ ████████╗████████╗ ██████╗ █████╗ ██╗",
|
|
25
|
+
"██╔═══██╗╚══██╔══╝╚══██╔══╝██╔═══██╗ ██╔══██╗██║",
|
|
26
|
+
"██║ ██║ ██║ ██║ ██║ ██║ ███████║██║",
|
|
27
|
+
"██║ ██║ ██║ ██║ ██║ ██║ ██╔══██║██║",
|
|
28
|
+
"╚██████╔╝ ██║ ██║ ╚██████╔╝ ██║ ██║██║",
|
|
29
|
+
" ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝",
|
|
30
|
+
];
|
|
31
|
+
class CliRuntimeSession {
|
|
32
|
+
config;
|
|
33
|
+
runtime = null;
|
|
34
|
+
runtimeTask = null;
|
|
35
|
+
status = "offline";
|
|
36
|
+
detail = "Aguardando pairing.";
|
|
37
|
+
lastError = null;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
}
|
|
41
|
+
handleRuntimeEvent(event) {
|
|
42
|
+
switch (event.type) {
|
|
43
|
+
case "starting":
|
|
44
|
+
this.status = "starting";
|
|
45
|
+
this.detail = "Conectando o runtime local do Otto Bridge...";
|
|
46
|
+
return;
|
|
47
|
+
case "connected":
|
|
48
|
+
this.status = "online";
|
|
49
|
+
this.detail = "Runtime conectado ao backend e pronto para handoff local.";
|
|
50
|
+
return;
|
|
51
|
+
case "server_hello":
|
|
52
|
+
this.status = "online";
|
|
53
|
+
this.detail = "Handshake com o backend concluído.";
|
|
54
|
+
return;
|
|
55
|
+
case "reconnecting":
|
|
56
|
+
this.status = "reconnecting";
|
|
57
|
+
this.detail = `Reconectando em ${Math.max(1, Math.round(event.delayMs / 1000))}s...`;
|
|
58
|
+
return;
|
|
59
|
+
case "socket_error":
|
|
60
|
+
this.status = "error";
|
|
61
|
+
this.lastError = event.message;
|
|
62
|
+
this.detail = truncate(event.message || "Falha de conexão do runtime.", 160);
|
|
63
|
+
return;
|
|
64
|
+
case "socket_closed":
|
|
65
|
+
if (this.status !== "error") {
|
|
66
|
+
this.status = "offline";
|
|
67
|
+
this.detail = `Socket fechado (code=${event.code}).`;
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
case "update_required":
|
|
71
|
+
case "update_available":
|
|
72
|
+
this.detail = truncate(event.message || this.detail, 160);
|
|
73
|
+
return;
|
|
74
|
+
default:
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async ensureStarted() {
|
|
79
|
+
if (this.runtimeTask) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.status = "starting";
|
|
83
|
+
this.detail = "Subindo runtime local do Otto Bridge...";
|
|
84
|
+
this.runtime = new BridgeRuntime(this.config, undefined, {
|
|
85
|
+
logger: {
|
|
86
|
+
info: () => undefined,
|
|
87
|
+
warn: () => undefined,
|
|
88
|
+
error: () => undefined,
|
|
89
|
+
event: (event) => {
|
|
90
|
+
this.handleRuntimeEvent(event);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
this.runtimeTask = this.runtime.start().catch((error) => {
|
|
95
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
96
|
+
this.status = "error";
|
|
97
|
+
this.lastError = detail;
|
|
98
|
+
this.detail = truncate(detail || "Falha ao iniciar o runtime.", 160);
|
|
99
|
+
});
|
|
100
|
+
await delay(350);
|
|
101
|
+
}
|
|
102
|
+
async replaceConfig(nextConfig) {
|
|
103
|
+
await this.stop();
|
|
104
|
+
this.config = nextConfig;
|
|
105
|
+
this.status = "offline";
|
|
106
|
+
this.detail = "Reinicializando runtime com o novo pairing...";
|
|
107
|
+
this.lastError = null;
|
|
108
|
+
await this.ensureStarted();
|
|
109
|
+
}
|
|
110
|
+
async stop() {
|
|
111
|
+
const runtime = this.runtime;
|
|
112
|
+
const runtimeTask = this.runtimeTask;
|
|
113
|
+
this.runtime = null;
|
|
114
|
+
this.runtimeTask = null;
|
|
115
|
+
if (runtime) {
|
|
116
|
+
await runtime.stop().catch(() => undefined);
|
|
117
|
+
}
|
|
118
|
+
if (runtimeTask) {
|
|
119
|
+
await runtimeTask.catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
if (this.status !== "error") {
|
|
122
|
+
this.status = "offline";
|
|
123
|
+
this.detail = "Runtime local desligado.";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
getStatusLabel() {
|
|
127
|
+
if (this.status === "online") {
|
|
128
|
+
return "online";
|
|
129
|
+
}
|
|
130
|
+
if (this.status === "starting") {
|
|
131
|
+
return "starting";
|
|
132
|
+
}
|
|
133
|
+
if (this.status === "reconnecting") {
|
|
134
|
+
return "reconnecting";
|
|
135
|
+
}
|
|
136
|
+
if (this.status === "error") {
|
|
137
|
+
return "error";
|
|
138
|
+
}
|
|
139
|
+
return "offline";
|
|
140
|
+
}
|
|
141
|
+
getStatusDetail() {
|
|
142
|
+
return this.detail;
|
|
143
|
+
}
|
|
144
|
+
getLastError() {
|
|
145
|
+
return this.lastError;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function style(text, color, enabled = true) {
|
|
149
|
+
if (!enabled) {
|
|
150
|
+
return text;
|
|
151
|
+
}
|
|
152
|
+
return `${color}${text}${ANSI.reset}`;
|
|
153
|
+
}
|
|
154
|
+
function supportsAnsi() {
|
|
155
|
+
return Boolean(output.isTTY);
|
|
156
|
+
}
|
|
157
|
+
function renderBanner() {
|
|
158
|
+
const enabled = supportsAnsi();
|
|
159
|
+
const lines = OTTOAI_BANNER.map((line) => style(line, ANSI.coral, enabled));
|
|
160
|
+
const title = `${BRIDGE_PACKAGE_NAME} v${BRIDGE_VERSION}`;
|
|
161
|
+
const subtitle = "Terminal bridge, pairing wizard and local Otto console";
|
|
162
|
+
return [
|
|
163
|
+
lines.join("\n"),
|
|
164
|
+
"",
|
|
165
|
+
`${style("OTTO BRIDGE", ANSI.blue, enabled)} ${style(title, ANSI.white, enabled)}`,
|
|
166
|
+
`${style(subtitle, ANSI.dim, enabled)}`,
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
function printSection(title) {
|
|
170
|
+
const enabled = supportsAnsi();
|
|
171
|
+
console.log(`\n${style(title, ANSI.blue, enabled)}`);
|
|
172
|
+
}
|
|
173
|
+
function printMuted(message) {
|
|
174
|
+
console.log(style(message, ANSI.dim, supportsAnsi()));
|
|
175
|
+
}
|
|
176
|
+
function printSuccess(message) {
|
|
177
|
+
console.log(style(message, ANSI.green, supportsAnsi()));
|
|
178
|
+
}
|
|
179
|
+
function printWarning(message) {
|
|
180
|
+
console.log(style(message, ANSI.amber, supportsAnsi()));
|
|
181
|
+
}
|
|
182
|
+
function printError(message) {
|
|
183
|
+
console.log(style(message, ANSI.red, supportsAnsi()));
|
|
184
|
+
}
|
|
185
|
+
function normalizeText(value) {
|
|
186
|
+
return String(value || "").trim();
|
|
187
|
+
}
|
|
188
|
+
function truncate(text, max = 180) {
|
|
189
|
+
const value = normalizeText(text);
|
|
190
|
+
if (value.length <= max) {
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
return `${value.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
194
|
+
}
|
|
195
|
+
function delay(ms) {
|
|
196
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
197
|
+
}
|
|
198
|
+
async function createPromptInterface() {
|
|
199
|
+
return createInterface({
|
|
200
|
+
input,
|
|
201
|
+
output,
|
|
202
|
+
terminal: true,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function ask(rl, label, options) {
|
|
206
|
+
const defaultValue = normalizeText(options?.defaultValue);
|
|
207
|
+
const suffix = defaultValue ? ` ${style(`[${defaultValue}]`, ANSI.dim, supportsAnsi())}` : "";
|
|
208
|
+
const answer = normalizeText(await rl.question(`${style("›", ANSI.coral, supportsAnsi())} ${label}${suffix}: `));
|
|
209
|
+
if (answer) {
|
|
210
|
+
return answer;
|
|
211
|
+
}
|
|
212
|
+
if (defaultValue) {
|
|
213
|
+
return defaultValue;
|
|
214
|
+
}
|
|
215
|
+
if (options?.allowEmpty) {
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
return await ask(rl, label, options);
|
|
219
|
+
}
|
|
220
|
+
async function askYesNo(rl, question, defaultValue = true) {
|
|
221
|
+
const answer = normalizeText(await rl.question(`${style("?", ANSI.blue, supportsAnsi())} ${question} ${style(defaultValue ? "[Y/n]" : "[y/N]", ANSI.dim, supportsAnsi())}: `)).toLowerCase();
|
|
222
|
+
if (!answer) {
|
|
223
|
+
return defaultValue;
|
|
224
|
+
}
|
|
225
|
+
return ["y", "yes", "s", "sim"].includes(answer);
|
|
226
|
+
}
|
|
227
|
+
async function pauseForEnter(rl, message = "Pressione Enter para continuar") {
|
|
228
|
+
await rl.question(`${style("↵", ANSI.dim, supportsAnsi())} ${message}`);
|
|
229
|
+
}
|
|
230
|
+
async function chooseExecutor(rl, current) {
|
|
231
|
+
const defaultType = current?.type || resolveExecutorConfig().type;
|
|
232
|
+
console.log([
|
|
233
|
+
`${style("1.", ANSI.coral, supportsAnsi())} native-macos ${style("(Mac real, runtime local)", ANSI.dim, supportsAnsi())}`,
|
|
234
|
+
`${style("2.", ANSI.coral, supportsAnsi())} mock ${style("(ambiente de teste)", ANSI.dim, supportsAnsi())}`,
|
|
235
|
+
].join("\n"));
|
|
236
|
+
const selection = await ask(rl, "Executor", {
|
|
237
|
+
defaultValue: defaultType === "mock" ? "2" : "1",
|
|
238
|
+
});
|
|
239
|
+
if (selection === "2" || selection.toLowerCase() === "mock") {
|
|
240
|
+
return { type: "mock" };
|
|
241
|
+
}
|
|
242
|
+
return { type: "native-macos" };
|
|
243
|
+
}
|
|
244
|
+
function extractBridgeHandoffSummary(payload) {
|
|
245
|
+
const narrationContext = payload.narration_context && typeof payload.narration_context === "object"
|
|
246
|
+
? payload.narration_context
|
|
247
|
+
: {};
|
|
248
|
+
const plan = payload.plan && typeof payload.plan === "object"
|
|
249
|
+
? payload.plan
|
|
250
|
+
: {};
|
|
251
|
+
const job = payload.job && typeof payload.job === "object"
|
|
252
|
+
? payload.job
|
|
253
|
+
: {};
|
|
254
|
+
return normalizeText(narrationContext.summary
|
|
255
|
+
|| plan.summary
|
|
256
|
+
|| plan.assistant_message
|
|
257
|
+
|| extractJobSummary(job));
|
|
258
|
+
}
|
|
259
|
+
function extractJobStepId(job) {
|
|
260
|
+
return normalizeText(job.runtime_current_step_id
|
|
261
|
+
|| (job.payload && typeof job.payload === "object" ? job.payload.runtime_current_step_id : "")
|
|
262
|
+
|| ((job.payload && typeof job.payload === "object")
|
|
263
|
+
? (job.payload.runtime_state?.current_step_id)
|
|
264
|
+
: "")
|
|
265
|
+
|| "");
|
|
266
|
+
}
|
|
267
|
+
function extractJobSummary(job) {
|
|
268
|
+
const payload = job.payload && typeof job.payload === "object"
|
|
269
|
+
? job.payload
|
|
270
|
+
: {};
|
|
271
|
+
const result = job.result && typeof job.result === "object"
|
|
272
|
+
? job.result
|
|
273
|
+
: {};
|
|
274
|
+
const outcome = result.outcome && typeof result.outcome === "object"
|
|
275
|
+
? result.outcome
|
|
276
|
+
: {};
|
|
277
|
+
return normalizeText(result.summary
|
|
278
|
+
|| outcome.summary
|
|
279
|
+
|| payload.kernel_intro_summary
|
|
280
|
+
|| payload.kernel_confirmation_summary
|
|
281
|
+
|| payload.planner_summary);
|
|
282
|
+
}
|
|
283
|
+
function extractConfirmationPrompt(job) {
|
|
284
|
+
const confirmationContext = job.confirmation_context && typeof job.confirmation_context === "object"
|
|
285
|
+
? job.confirmation_context
|
|
286
|
+
: {};
|
|
287
|
+
const payload = job.payload && typeof job.payload === "object"
|
|
288
|
+
? job.payload
|
|
289
|
+
: {};
|
|
290
|
+
return normalizeText(confirmationContext.message
|
|
291
|
+
|| payload.kernel_confirmation_summary
|
|
292
|
+
|| payload.confirmation_message) || "O Otto está aguardando sua confirmação para continuar.";
|
|
293
|
+
}
|
|
294
|
+
function renderStatusOverview(config, runtimeSession) {
|
|
295
|
+
return [
|
|
296
|
+
`${style("Device", ANSI.blue, supportsAnsi())}: ${config.deviceName}`,
|
|
297
|
+
`${style("Device ID", ANSI.blue, supportsAnsi())}: ${config.deviceId}`,
|
|
298
|
+
`${style("API", ANSI.blue, supportsAnsi())}: ${config.apiBaseUrl}`,
|
|
299
|
+
`${style("Executor", ANSI.blue, supportsAnsi())}: ${config.executor.type}`,
|
|
300
|
+
`${style("Approval", ANSI.blue, supportsAnsi())}: ${config.approvalMode}`,
|
|
301
|
+
`${style("Runtime", ANSI.blue, supportsAnsi())}: ${runtimeSession?.getStatusLabel() || "offline"}`,
|
|
302
|
+
...(runtimeSession?.getStatusDetail()
|
|
303
|
+
? [`${style("Runtime note", ANSI.blue, supportsAnsi())}: ${runtimeSession.getStatusDetail()}`]
|
|
304
|
+
: []),
|
|
305
|
+
`${style("Config", ANSI.blue, supportsAnsi())}: ${getBridgeConfigPath()}`,
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
async function printExtensionsOverview(config) {
|
|
309
|
+
printSection("Extensions");
|
|
310
|
+
if (!config.installedExtensions.length) {
|
|
311
|
+
printMuted("Nenhuma extensão instalada neste bridge.");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
for (const extension of config.installedExtensions) {
|
|
315
|
+
if (!isManagedBridgeExtensionSlug(extension)) {
|
|
316
|
+
console.log(`- ${extension}`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const state = await loadManagedBridgeExtensionState(extension);
|
|
320
|
+
const status = state ? formatManagedBridgeExtensionStatus(state.status) : "sem estado salvo";
|
|
321
|
+
console.log(`- ${extension}: ${status}`);
|
|
322
|
+
if (state?.notes) {
|
|
323
|
+
printMuted(` ${truncate(state.notes, 140)}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function runSetupWizard(rl, options) {
|
|
328
|
+
printSection(options?.postinstall ? "Setup Inicial" : "Pairing Setup");
|
|
329
|
+
printMuted("Cole o pairing code gerado na interface web do Otto.");
|
|
330
|
+
const existingConfig = await loadBridgeConfig();
|
|
331
|
+
if (existingConfig) {
|
|
332
|
+
const keepExisting = await askYesNo(rl, `Já existe um pairing salvo para ${existingConfig.deviceName}. Quer substituir esse vínculo`, false);
|
|
333
|
+
if (!keepExisting) {
|
|
334
|
+
return { config: existingConfig, openConsole: false };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const apiBaseUrl = resolveApiBaseUrl(await ask(rl, "API base URL", {
|
|
338
|
+
defaultValue: existingConfig?.apiBaseUrl || DEFAULT_API_BASE_URL,
|
|
339
|
+
}));
|
|
340
|
+
const pairingCode = await ask(rl, "Pairing code", {
|
|
341
|
+
allowEmpty: Boolean(options?.postinstall),
|
|
342
|
+
});
|
|
343
|
+
if (!pairingCode) {
|
|
344
|
+
printMuted("Setup adiado. Quando quiser, rode `otto-bridge setup`.");
|
|
345
|
+
return { config: existingConfig || null, openConsole: false };
|
|
346
|
+
}
|
|
347
|
+
const deviceName = await ask(rl, "Nome do dispositivo", {
|
|
348
|
+
defaultValue: existingConfig?.deviceName || defaultDeviceName(),
|
|
349
|
+
});
|
|
350
|
+
const executor = await chooseExecutor(rl, existingConfig?.executor);
|
|
351
|
+
printMuted("Solicitando pairing ao backend e aguardando aprovação...");
|
|
352
|
+
const config = await pairDevice({
|
|
353
|
+
apiBaseUrl,
|
|
354
|
+
pairingCode,
|
|
355
|
+
deviceName,
|
|
356
|
+
executor,
|
|
357
|
+
});
|
|
358
|
+
printSuccess(`Bridge pareado com sucesso como ${config.deviceName}.`);
|
|
359
|
+
printMuted(`Config salvo em ${getBridgeConfigPath()}`);
|
|
360
|
+
const openConsole = await askYesNo(rl, "Abrir o Otto Console agora", true);
|
|
361
|
+
return { config, openConsole };
|
|
362
|
+
}
|
|
363
|
+
async function followConsoleJob(rl, config, jobId) {
|
|
364
|
+
let lastStatus = "";
|
|
365
|
+
let lastStepId = "";
|
|
366
|
+
let awaitingDecision = false;
|
|
367
|
+
for (;;) {
|
|
368
|
+
const envelope = await getRuntimeCliJob(config, jobId);
|
|
369
|
+
const job = envelope.job || {};
|
|
370
|
+
const status = normalizeText(job.status).toLowerCase() || "unknown";
|
|
371
|
+
const stepId = extractJobStepId(job);
|
|
372
|
+
if (status !== lastStatus || stepId !== lastStepId) {
|
|
373
|
+
const statusLabel = `${status}${stepId ? ` · ${stepId}` : ""}`;
|
|
374
|
+
console.log(`${style("runtime", ANSI.teal, supportsAnsi())} ${statusLabel}`);
|
|
375
|
+
lastStatus = status;
|
|
376
|
+
lastStepId = stepId;
|
|
377
|
+
}
|
|
378
|
+
if (status === "confirm_required" && !awaitingDecision) {
|
|
379
|
+
awaitingDecision = true;
|
|
380
|
+
console.log(style(extractConfirmationPrompt(job), ANSI.amber, supportsAnsi()));
|
|
381
|
+
const approve = await askYesNo(rl, "Aprovar este passo", true);
|
|
382
|
+
if (approve) {
|
|
383
|
+
await confirmRuntimeCliJob(config, jobId, "approve");
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const reject = await askYesNo(rl, "Rejeitar explicitamente este passo", true);
|
|
387
|
+
if (reject) {
|
|
388
|
+
await confirmRuntimeCliJob(config, jobId, "reject");
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
await cancelRuntimeCliJob(config, jobId, "Cancelado no Otto Console");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
awaitingDecision = false;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
398
|
+
const summary = extractJobSummary(job)
|
|
399
|
+
|| (status === "completed"
|
|
400
|
+
? "Execução local concluída."
|
|
401
|
+
: status === "failed"
|
|
402
|
+
? "Execução local falhou."
|
|
403
|
+
: "Execução local cancelada.");
|
|
404
|
+
console.log(`${style("otto", ANSI.coral, supportsAnsi())} ${summary}`);
|
|
405
|
+
return summary;
|
|
406
|
+
}
|
|
407
|
+
await delay(1400);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function runOttoConsole(rl, config, runtimeSession, options) {
|
|
411
|
+
printSection("Otto Console");
|
|
412
|
+
printMuted("Este console usa o mesmo runtime local já ligado pelo `otto-bridge`.");
|
|
413
|
+
printMuted(`Runtime: ${runtimeSession.getStatusLabel()} · ${runtimeSession.getStatusDetail()}`);
|
|
414
|
+
const sessionId = randomUUID();
|
|
415
|
+
const conversation = [];
|
|
416
|
+
const printConsoleHelp = () => {
|
|
417
|
+
printMuted("Comandos: /help, /clear, /status, /exit");
|
|
418
|
+
};
|
|
419
|
+
const handlePrompt = async (promptText) => {
|
|
420
|
+
const normalizedPrompt = normalizeText(promptText);
|
|
421
|
+
if (!normalizedPrompt) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (normalizedPrompt === "/help") {
|
|
425
|
+
printConsoleHelp();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (normalizedPrompt === "/clear") {
|
|
429
|
+
conversation.splice(0, conversation.length);
|
|
430
|
+
printMuted("Contexto local do console limpo.");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (normalizedPrompt === "/status") {
|
|
434
|
+
renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
|
|
435
|
+
const runtimeFailure = runtimeSession.getLastError();
|
|
436
|
+
if (runtimeFailure) {
|
|
437
|
+
printWarning(`Runtime reportou erro: ${runtimeFailure}`);
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (normalizedPrompt === "/exit") {
|
|
442
|
+
throw new Error("__OTTO_CONSOLE_EXIT__");
|
|
443
|
+
}
|
|
444
|
+
conversation.push({ role: "user", content: normalizedPrompt });
|
|
445
|
+
while (conversation.length > 18) {
|
|
446
|
+
conversation.shift();
|
|
447
|
+
}
|
|
448
|
+
console.log(`${style("você", ANSI.white, supportsAnsi())} ${normalizedPrompt}`);
|
|
449
|
+
let streamedAssistant = "";
|
|
450
|
+
let assistantPrefixPrinted = false;
|
|
451
|
+
let handoffPayload = null;
|
|
452
|
+
await streamDeviceCliChat(config, {
|
|
453
|
+
messages: conversation,
|
|
454
|
+
session_id: sessionId,
|
|
455
|
+
}, async (event) => {
|
|
456
|
+
const chunkType = normalizeText(event.chunk_type).toLowerCase();
|
|
457
|
+
const eventType = normalizeText(event.type).toLowerCase();
|
|
458
|
+
if (chunkType === "bridge_handoff") {
|
|
459
|
+
handoffPayload = event;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (chunkType === "search_status") {
|
|
463
|
+
const status = normalizeText(event.status);
|
|
464
|
+
if (status) {
|
|
465
|
+
printMuted(`Busca: ${status}`);
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const errorMessage = normalizeText(event.error || (eventType === "error" ? event.content : ""));
|
|
470
|
+
if (errorMessage) {
|
|
471
|
+
throw new Error(errorMessage);
|
|
472
|
+
}
|
|
473
|
+
const contentChunk = typeof event.content === "string" ? event.content : "";
|
|
474
|
+
if (!contentChunk) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!assistantPrefixPrinted) {
|
|
478
|
+
output.write(`${style("otto", ANSI.coral, supportsAnsi())} `);
|
|
479
|
+
assistantPrefixPrinted = true;
|
|
480
|
+
}
|
|
481
|
+
output.write(contentChunk);
|
|
482
|
+
streamedAssistant += contentChunk;
|
|
483
|
+
});
|
|
484
|
+
if (assistantPrefixPrinted) {
|
|
485
|
+
output.write("\n");
|
|
486
|
+
}
|
|
487
|
+
let finalAssistantSummary = normalizeText(streamedAssistant);
|
|
488
|
+
if (handoffPayload) {
|
|
489
|
+
const handoffData = handoffPayload;
|
|
490
|
+
const bridgeSummary = extractBridgeHandoffSummary(handoffData);
|
|
491
|
+
if (bridgeSummary) {
|
|
492
|
+
console.log(`${style("otto", ANSI.coral, supportsAnsi())} ${bridgeSummary}`);
|
|
493
|
+
}
|
|
494
|
+
const job = handoffData.job && typeof handoffData.job === "object"
|
|
495
|
+
? handoffData.job
|
|
496
|
+
: null;
|
|
497
|
+
const jobId = normalizeText(job?.id);
|
|
498
|
+
if (jobId) {
|
|
499
|
+
finalAssistantSummary = await followConsoleJob(rl, config, jobId);
|
|
500
|
+
}
|
|
501
|
+
else if (bridgeSummary) {
|
|
502
|
+
finalAssistantSummary = bridgeSummary;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (finalAssistantSummary) {
|
|
506
|
+
conversation.push({ role: "assistant", content: finalAssistantSummary });
|
|
507
|
+
while (conversation.length > 18) {
|
|
508
|
+
conversation.shift();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
printConsoleHelp();
|
|
513
|
+
if (options?.initialPrompt) {
|
|
514
|
+
await handlePrompt(options.initialPrompt);
|
|
515
|
+
}
|
|
516
|
+
for (;;) {
|
|
517
|
+
const promptText = await ask(rl, "OTTO", { allowEmpty: true });
|
|
518
|
+
try {
|
|
519
|
+
await handlePrompt(promptText);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
if (error instanceof Error && error.message === "__OTTO_CONSOLE_EXIT__") {
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function printStatusView(rl, config, runtimeSession) {
|
|
530
|
+
printSection("Bridge Status");
|
|
531
|
+
renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
|
|
532
|
+
await printExtensionsOverview(config);
|
|
533
|
+
await pauseForEnter(rl);
|
|
534
|
+
}
|
|
535
|
+
async function pickHomeChoice(rl, paired) {
|
|
536
|
+
printSection("Home");
|
|
537
|
+
const options = paired
|
|
538
|
+
? [
|
|
539
|
+
`${style("1.", ANSI.coral, supportsAnsi())} Otto Console`,
|
|
540
|
+
`${style("2.", ANSI.coral, supportsAnsi())} Re-pair / setup`,
|
|
541
|
+
`${style("3.", ANSI.coral, supportsAnsi())} Status do bridge`,
|
|
542
|
+
`${style("4.", ANSI.coral, supportsAnsi())} Extensões instaladas`,
|
|
543
|
+
`${style("5.", ANSI.coral, supportsAnsi())} Sair`,
|
|
544
|
+
]
|
|
545
|
+
: [
|
|
546
|
+
`${style("1.", ANSI.coral, supportsAnsi())} Pairing setup`,
|
|
547
|
+
`${style("2.", ANSI.coral, supportsAnsi())} Sair`,
|
|
548
|
+
];
|
|
549
|
+
console.log(options.join("\n"));
|
|
550
|
+
const answer = await ask(rl, "Escolha");
|
|
551
|
+
if (!paired) {
|
|
552
|
+
return answer === "1" ? "setup" : "exit";
|
|
553
|
+
}
|
|
554
|
+
if (answer === "1")
|
|
555
|
+
return "console";
|
|
556
|
+
if (answer === "2")
|
|
557
|
+
return "setup";
|
|
558
|
+
if (answer === "3")
|
|
559
|
+
return "status";
|
|
560
|
+
if (answer === "4")
|
|
561
|
+
return "extensions";
|
|
562
|
+
return "exit";
|
|
563
|
+
}
|
|
564
|
+
export async function launchInteractiveCli(options) {
|
|
565
|
+
const rl = await createPromptInterface();
|
|
566
|
+
let runtimeSession = null;
|
|
567
|
+
try {
|
|
568
|
+
console.clear();
|
|
569
|
+
console.log(renderBanner());
|
|
570
|
+
let config = await loadBridgeConfig();
|
|
571
|
+
if (!config) {
|
|
572
|
+
const setup = await runSetupWizard(rl, options);
|
|
573
|
+
config = setup.config;
|
|
574
|
+
if (config && setup.openConsole) {
|
|
575
|
+
runtimeSession = new CliRuntimeSession(config);
|
|
576
|
+
await runtimeSession.ensureStarted();
|
|
577
|
+
await runOttoConsole(rl, config, runtimeSession);
|
|
578
|
+
}
|
|
579
|
+
if (!config) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
runtimeSession = runtimeSession || new CliRuntimeSession(config);
|
|
584
|
+
await runtimeSession.ensureStarted();
|
|
585
|
+
for (;;) {
|
|
586
|
+
console.log("");
|
|
587
|
+
renderStatusOverview(config, runtimeSession).forEach((line) => console.log(line));
|
|
588
|
+
const choice = await pickHomeChoice(rl, true);
|
|
589
|
+
if (choice === "exit") {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
if (choice === "setup") {
|
|
593
|
+
const setup = await runSetupWizard(rl);
|
|
594
|
+
if (setup.config) {
|
|
595
|
+
config = setup.config;
|
|
596
|
+
if (runtimeSession) {
|
|
597
|
+
await runtimeSession.replaceConfig(setup.config);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
runtimeSession = new CliRuntimeSession(setup.config);
|
|
601
|
+
await runtimeSession.ensureStarted();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (setup.config && setup.openConsole && runtimeSession) {
|
|
605
|
+
await runOttoConsole(rl, setup.config, runtimeSession);
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (choice === "console" && runtimeSession) {
|
|
610
|
+
await runOttoConsole(rl, config, runtimeSession);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (choice === "status" && runtimeSession) {
|
|
614
|
+
await printStatusView(rl, config, runtimeSession);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (choice === "extensions") {
|
|
618
|
+
await printExtensionsOverview(config);
|
|
619
|
+
await pauseForEnter(rl);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
finally {
|
|
624
|
+
await runtimeSession?.stop().catch(() => undefined);
|
|
625
|
+
rl.close();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
export async function runSetupCommand(options) {
|
|
629
|
+
const rl = await createPromptInterface();
|
|
630
|
+
let runtimeSession = null;
|
|
631
|
+
try {
|
|
632
|
+
console.clear();
|
|
633
|
+
console.log(renderBanner());
|
|
634
|
+
const setup = await runSetupWizard(rl, options);
|
|
635
|
+
if (setup.config && setup.openConsole) {
|
|
636
|
+
runtimeSession = new CliRuntimeSession(setup.config);
|
|
637
|
+
await runtimeSession.ensureStarted();
|
|
638
|
+
await runOttoConsole(rl, setup.config, runtimeSession);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
await runtimeSession?.stop().catch(() => undefined);
|
|
643
|
+
rl.close();
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
export async function runConsoleCommand(initialPrompt) {
|
|
647
|
+
const rl = await createPromptInterface();
|
|
648
|
+
let runtimeSession = null;
|
|
649
|
+
try {
|
|
650
|
+
console.clear();
|
|
651
|
+
console.log(renderBanner());
|
|
652
|
+
let config = await loadBridgeConfig();
|
|
653
|
+
if (!config) {
|
|
654
|
+
const setup = await runSetupWizard(rl);
|
|
655
|
+
if (!setup.config) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
config = setup.config;
|
|
659
|
+
}
|
|
660
|
+
runtimeSession = new CliRuntimeSession(config);
|
|
661
|
+
await runtimeSession.ensureStarted();
|
|
662
|
+
await runOttoConsole(rl, config, runtimeSession, { initialPrompt });
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
await runtimeSession?.stop().catch(() => undefined);
|
|
666
|
+
rl.close();
|
|
667
|
+
}
|
|
668
|
+
}
|