@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.
@@ -231,8 +231,12 @@ function isSupportedWhatsAppInboxAutomation(automation) {
231
231
  const bridgeConfig = automation.bridge_config || {};
232
232
  return String(bridgeConfig.monitor_scope || "").trim().toLowerCase() === "inbox";
233
233
  }
234
+ function isSupportedBridgeAutomation(automation) {
235
+ return normalizeChannel(automation.channel) === "bridge";
236
+ }
234
237
  export class LocalAutomationRuntime {
235
238
  config;
239
+ logger;
236
240
  automations = new Map();
237
241
  states = new Map();
238
242
  syncTimer = null;
@@ -242,8 +246,12 @@ export class LocalAutomationRuntime {
242
246
  started = false;
243
247
  stopped = false;
244
248
  whatsappBrowser = null;
245
- constructor(config) {
249
+ constructor(config, logger) {
246
250
  this.config = config;
251
+ this.logger = logger;
252
+ }
253
+ logWarn(message) {
254
+ (this.logger?.warn || console.warn)(message);
247
255
  }
248
256
  async start() {
249
257
  if (this.started) {
@@ -253,11 +261,11 @@ export class LocalAutomationRuntime {
253
261
  this.stopped = false;
254
262
  await this.syncAutomations().catch((error) => {
255
263
  const detail = error instanceof Error ? error.message : String(error);
256
- console.warn(`[otto-bridge] local automations sync failed: ${detail}`);
264
+ this.logWarn(`[otto-bridge] local automations sync failed: ${detail}`);
257
265
  });
258
266
  await this.tick().catch((error) => {
259
267
  const detail = error instanceof Error ? error.message : String(error);
260
- console.warn(`[otto-bridge] local automations tick failed: ${detail}`);
268
+ this.logWarn(`[otto-bridge] local automations tick failed: ${detail}`);
261
269
  });
262
270
  this.syncTimer = setInterval(() => {
263
271
  void this.syncAutomations();
@@ -329,7 +337,7 @@ export class LocalAutomationRuntime {
329
337
  }
330
338
  catch (error) {
331
339
  const detail = error instanceof Error ? error.message : String(error);
332
- console.warn(`[otto-bridge] local automations sync failed: ${detail}`);
340
+ this.logWarn(`[otto-bridge] local automations sync failed: ${detail}`);
333
341
  }
334
342
  finally {
335
343
  this.syncInFlight = false;
@@ -353,7 +361,10 @@ export class LocalAutomationRuntime {
353
361
  }
354
362
  state.running = true;
355
363
  try {
356
- if (isSupportedWhatsAppContactAutomation(automation)) {
364
+ if (isSupportedBridgeAutomation(automation)) {
365
+ await this.handleBridgeAutomation(automation);
366
+ }
367
+ else if (isSupportedWhatsAppContactAutomation(automation)) {
357
368
  await this.handleWhatsAppContactAutomation(automation, state);
358
369
  }
359
370
  else if (isSupportedWhatsAppInboxAutomation(automation)) {
@@ -362,7 +373,7 @@ export class LocalAutomationRuntime {
362
373
  }
363
374
  catch (error) {
364
375
  const detail = error instanceof Error ? error.message : String(error);
365
- console.warn(`[otto-bridge] local automation failed id=${automationId}: ${detail}`);
376
+ this.logWarn(`[otto-bridge] local automation failed id=${automationId}: ${detail}`);
366
377
  }
367
378
  finally {
368
379
  state.running = false;
@@ -395,7 +406,7 @@ export class LocalAutomationRuntime {
395
406
  await browser.ensureReady();
396
407
  const selected = await browser.selectConversation(contact);
397
408
  if (!selected) {
398
- console.warn(`[otto-bridge] local whatsapp automation could not find contact="${contact}"`);
409
+ this.logWarn(`[otto-bridge] local whatsapp automation could not find contact="${contact}"`);
399
410
  return;
400
411
  }
401
412
  await this.processWhatsAppConversation(automation, state, browser, contact, { alreadySelected: true });
@@ -424,7 +435,7 @@ export class LocalAutomationRuntime {
424
435
  try {
425
436
  const selected = await browser.selectConversation(contact);
426
437
  if (!selected) {
427
- console.warn(`[otto-bridge] local whatsapp inbox automation could not find contact="${contact}"`);
438
+ this.logWarn(`[otto-bridge] local whatsapp inbox automation could not find contact="${contact}"`);
428
439
  continue;
429
440
  }
430
441
  await this.processWhatsAppConversation(automation, state, browser, contact, {
@@ -434,10 +445,21 @@ export class LocalAutomationRuntime {
434
445
  }
435
446
  catch (error) {
436
447
  const detail = error instanceof Error ? error.message : String(error);
437
- console.warn(`[otto-bridge] local whatsapp inbox conversation failed contact="${contact}": ${detail}`);
448
+ this.logWarn(`[otto-bridge] local whatsapp inbox conversation failed contact="${contact}": ${detail}`);
438
449
  }
439
450
  }
440
451
  }
452
+ async handleBridgeAutomation(automation) {
453
+ const automationId = String(automation.id || "").trim();
454
+ if (!automationId) {
455
+ return;
456
+ }
457
+ await postDeviceJson(this.config.apiBaseUrl, this.config.deviceToken, "/v1/devices/automations/local/bridge/trigger", {
458
+ automation_id: automationId,
459
+ observed_at: new Date().toISOString(),
460
+ reason: "schedule_tick",
461
+ });
462
+ }
441
463
  isPrimaryContactForAutomation(automation, contact) {
442
464
  if (!isSupportedWhatsAppContactAutomation(automation)) {
443
465
  return false;
@@ -478,7 +500,7 @@ export class LocalAutomationRuntime {
478
500
  if (!options?.alreadySelected) {
479
501
  const selected = await browser.selectConversation(contact);
480
502
  if (!selected) {
481
- console.warn(`[otto-bridge] local whatsapp automation could not find contact="${contact}"`);
503
+ this.logWarn(`[otto-bridge] local whatsapp automation could not find contact="${contact}"`);
482
504
  return;
483
505
  }
484
506
  }
@@ -552,7 +574,7 @@ export class LocalAutomationRuntime {
552
574
  }
553
575
  catch (error) {
554
576
  const detail = error instanceof Error ? error.message : String(error);
555
- console.warn(`[otto-bridge] local whatsapp completion failed id=${automation.id}: ${detail}`);
577
+ this.logWarn(`[otto-bridge] local whatsapp completion failed id=${automation.id}: ${detail}`);
556
578
  this.rememberDeltaHash(automation, state, contact, sent ? completionDeltaHash : deltaHash);
557
579
  }
558
580
  }
package/dist/main.js CHANGED
@@ -7,10 +7,15 @@ import { pairDevice } from "./pairing.js";
7
7
  import { BridgeRuntime } from "./runtime.js";
8
8
  import { detectWhatsAppBackgroundStatus, runWhatsAppBackgroundSetup, } from "./whatsapp_background.js";
9
9
  import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
10
+ import { launchInteractiveCli, runConsoleCommand, runSetupCommand, } from "./cli_terminal.js";
10
11
  const RUNTIME_STATUS_FRESHNESS_MS = 90_000;
11
12
  const UPDATE_RETRY_DELAYS_MS = [0, 8_000, 20_000];
12
13
  function parseArgs(argv) {
13
14
  const [maybeCommand, ...rest] = argv;
15
+ if (!maybeCommand) {
16
+ const interactiveDefault = process.stdout.isTTY && process.stdin.isTTY && process.env.OTTO_BRIDGE_LEGACY_DEFAULT_RUN !== "1";
17
+ return { command: interactiveDefault ? "home" : "run", options: new Map() };
18
+ }
14
19
  if (maybeCommand === "--help" || maybeCommand === "-h") {
15
20
  return { command: "help", options: new Map() };
16
21
  }
@@ -100,8 +105,11 @@ function resolveExecutorOverrides(args, current) {
100
105
  }
101
106
  function printUsage() {
102
107
  console.log(`Usage:
108
+ otto-bridge
109
+ otto-bridge home
110
+ otto-bridge setup
111
+ otto-bridge console
103
112
  otto-bridge pair --api http://localhost:8000 --code ABC123 [--name "Meu PC"] [--executor native-macos|mock|clawd-cursor]
104
- otto-bridge run [--executor native-macos|mock|clawd-cursor] [--clawd-url http://127.0.0.1:3847]
105
113
  otto-bridge status
106
114
  otto-bridge extensions --list
107
115
  otto-bridge extensions --install github
@@ -113,8 +121,10 @@ function printUsage() {
113
121
  otto-bridge unpair
114
122
 
115
123
  Examples:
124
+ otto-bridge
125
+ otto-bridge setup
126
+ otto-bridge console
116
127
  otto-bridge pair --api https://api.leg3ndy.com.br --code ABC123
117
- otto-bridge run
118
128
  otto-bridge extensions --install whatsappweb
119
129
  otto-bridge extensions --setup whatsappweb
120
130
  otto-bridge extensions --status whatsappweb
@@ -275,7 +285,7 @@ async function runPairCommand(args) {
275
285
  console.log(`[otto-bridge] paired device=${config.deviceId}`);
276
286
  console.log(`[otto-bridge] executor=${config.executor.type}`);
277
287
  console.log(`[otto-bridge] config=${getBridgeConfigPath()}`);
278
- console.log("[otto-bridge] next step: run `otto-bridge run` to keep this device online");
288
+ console.log("[otto-bridge] next step: run `otto-bridge` to abrir o hub e manter o runtime local online");
279
289
  }
280
290
  async function loadRequiredBridgeConfig() {
281
291
  const config = await loadBridgeConfig();
@@ -285,6 +295,7 @@ async function loadRequiredBridgeConfig() {
285
295
  return config;
286
296
  }
287
297
  async function runRuntimeCommand(args) {
298
+ console.log("[otto-bridge] `run` agora é um alias legado. Prefira `otto-bridge`.");
288
299
  const config = await loadRequiredBridgeConfig();
289
300
  const runtimeConfig = {
290
301
  ...config,
@@ -574,6 +585,17 @@ async function runUpdateCommand(args) {
574
585
  async function main() {
575
586
  const args = parseArgs(process.argv.slice(2));
576
587
  switch (args.command) {
588
+ case "home":
589
+ await launchInteractiveCli();
590
+ return;
591
+ case "setup":
592
+ await runSetupCommand({
593
+ postinstall: args.options.has("postinstall"),
594
+ });
595
+ return;
596
+ case "console":
597
+ await runConsoleCommand(option(args, "prompt"));
598
+ return;
577
599
  case "pair":
578
600
  await runPairCommand(args);
579
601
  return;
package/dist/runtime.js CHANGED
@@ -6,6 +6,7 @@ import { JobCancelledError } from "./executors/shared.js";
6
6
  import { isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
7
7
  import { LocalAutomationRuntime } from "./local_automations.js";
8
8
  import { buildLocalToolCatalog } from "./tool_catalog.js";
9
+ import { parseJobRuntimeManifest, runtimeStepIdForEvent, } from "./runtime_contract.js";
9
10
  function delay(ms) {
10
11
  return new Promise((resolve) => setTimeout(resolve, ms));
11
12
  }
@@ -55,6 +56,11 @@ function bridgeReleaseFromMessage(message) {
55
56
  }
56
57
  return null;
57
58
  }
59
+ function confirmationKey(jobId, stepId) {
60
+ const normalizedJobId = String(jobId || "").trim();
61
+ const normalizedStepId = String(stepId || "").trim();
62
+ return normalizedStepId ? `${normalizedJobId}:${normalizedStepId}` : normalizedJobId;
63
+ }
58
64
  async function parseSocketMessage(data) {
59
65
  if (typeof data === "string") {
60
66
  return JSON.parse(data);
@@ -72,6 +78,7 @@ async function parseSocketMessage(data) {
72
78
  }
73
79
  export class BridgeRuntime {
74
80
  config;
81
+ options;
75
82
  reconnectDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS;
76
83
  executor;
77
84
  localAutomationRuntime;
@@ -81,10 +88,23 @@ export class BridgeRuntime {
81
88
  started = false;
82
89
  pendingConfirmations = new Map();
83
90
  activeCancels = new Map();
84
- constructor(config, executor) {
91
+ constructor(config, executor, options = {}) {
85
92
  this.config = config;
93
+ this.options = options;
86
94
  this.executor = executor ?? this.createDefaultExecutor(config);
87
- this.localAutomationRuntime = new LocalAutomationRuntime(config);
95
+ this.localAutomationRuntime = new LocalAutomationRuntime(config, this.options.logger);
96
+ }
97
+ logInfo(message) {
98
+ (this.options.logger?.info || console.log)(message);
99
+ }
100
+ logWarn(message) {
101
+ (this.options.logger?.warn || console.warn)(message);
102
+ }
103
+ logError(message) {
104
+ (this.options.logger?.error || console.error)(message);
105
+ }
106
+ emit(event) {
107
+ this.options.logger?.event?.(event);
88
108
  }
89
109
  async buildHelloMetadata() {
90
110
  const metadata = {
@@ -163,10 +183,11 @@ export class BridgeRuntime {
163
183
  }
164
184
  await this.localAutomationRuntime.start().catch((error) => {
165
185
  const detail = error instanceof Error ? error.message : String(error);
166
- console.error(`[otto-bridge] local automation runtime failed to start: ${detail}`);
186
+ this.logError(`[otto-bridge] local automation runtime failed to start: ${detail}`);
167
187
  });
168
188
  }
169
- console.log(`[otto-bridge] runtime start device=${this.config.deviceId}`);
189
+ this.logInfo(`[otto-bridge] runtime start device=${this.config.deviceId}`);
190
+ this.emit({ type: "starting", deviceId: this.config.deviceId });
170
191
  while (!this.stopped) {
171
192
  try {
172
193
  await this.connectOnce();
@@ -177,12 +198,14 @@ export class BridgeRuntime {
177
198
  break;
178
199
  }
179
200
  const message = error instanceof Error ? error.message : String(error);
180
- console.error(`[otto-bridge] socket error: ${message}`);
201
+ this.logError(`[otto-bridge] socket error: ${message}`);
202
+ this.emit({ type: "socket_error", message });
181
203
  }
182
204
  if (this.stopped) {
183
205
  break;
184
206
  }
185
- console.log(`[otto-bridge] reconnecting in ${this.reconnectDelayMs}ms`);
207
+ this.logInfo(`[otto-bridge] reconnecting in ${this.reconnectDelayMs}ms`);
208
+ this.emit({ type: "reconnecting", delayMs: this.reconnectDelayMs });
186
209
  await delay(this.reconnectDelayMs);
187
210
  this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, DEFAULT_RECONNECT_MAX_DELAY_MS);
188
211
  }
@@ -232,10 +255,11 @@ export class BridgeRuntime {
232
255
  };
233
256
  return await new Promise((resolve, reject) => {
234
257
  socket.addEventListener("open", () => {
235
- console.log(`[otto-bridge] connected ws=${this.config.wsUrl}`);
258
+ this.logInfo(`[otto-bridge] connected ws=${this.config.wsUrl}`);
259
+ this.emit({ type: "connected", wsUrl: this.config.wsUrl });
236
260
  this.sendHello(socket).catch((error) => {
237
261
  const detail = error instanceof Error ? error.message : String(error);
238
- console.error(`[otto-bridge] hello metadata failed: ${detail}`);
262
+ this.logError(`[otto-bridge] hello metadata failed: ${detail}`);
239
263
  });
240
264
  heartbeatTimer = setInterval(() => {
241
265
  if (socket.readyState === WebSocket.OPEN) {
@@ -254,14 +278,15 @@ export class BridgeRuntime {
254
278
  }
255
279
  catch (error) {
256
280
  const detail = error instanceof Error ? error.message : String(error);
257
- console.error(`[otto-bridge] invalid message: ${detail}`);
281
+ this.logError(`[otto-bridge] invalid message: ${detail}`);
258
282
  }
259
283
  });
260
284
  socket.addEventListener("close", (event) => {
261
285
  stopHeartbeat();
262
286
  rejectPendingConfirmations(new Error("WebSocket closed while awaiting confirmation"));
263
287
  this.activeSocket = null;
264
- console.log(`[otto-bridge] socket closed code=${event.code}`);
288
+ this.logInfo(`[otto-bridge] socket closed code=${event.code}`);
289
+ this.emit({ type: "socket_closed", code: event.code });
265
290
  resolve();
266
291
  });
267
292
  socket.addEventListener("error", () => {
@@ -282,14 +307,16 @@ export class BridgeRuntime {
282
307
  const type = String(message.type || "");
283
308
  switch (type) {
284
309
  case "device.hello":
285
- console.log(`[otto-bridge] server hello device=${String(message.device_id || "")}`);
310
+ this.logInfo(`[otto-bridge] server hello device=${String(message.device_id || "")}`);
311
+ this.emit({ type: "server_hello", deviceId: String(message.device_id || "") });
286
312
  return;
287
313
  case "device.hello_ack":
288
314
  this.maybeLogBridgeReleaseNotice(message);
289
315
  case "device.heartbeat_ack":
290
316
  return;
291
317
  case "device.job.start":
292
- console.log(`[otto-bridge] job start payload=${JSON.stringify(message)}`);
318
+ this.logInfo(`[otto-bridge] job start payload=${JSON.stringify(message)}`);
319
+ this.emit({ type: "job_start", jobId: String(message.job_id || "") });
293
320
  this.executeJob(socket, {
294
321
  job_id: String(message.job_id || ""),
295
322
  device_id: String(message.device_id || ""),
@@ -299,17 +326,17 @@ export class BridgeRuntime {
299
326
  : {},
300
327
  }).catch((error) => {
301
328
  const detail = error instanceof Error ? error.message : String(error);
302
- console.error(`[otto-bridge] executor error: ${detail}`);
329
+ this.logError(`[otto-bridge] executor error: ${detail}`);
303
330
  });
304
331
  return;
305
332
  case "device.job.confirmation":
306
333
  this.resolveConfirmation(message);
307
334
  return;
308
335
  case "device.job.cancel":
309
- await this.cancelJob(String(message.job_id || ""));
336
+ await this.cancelJob(String(message.job_id || ""), String(message.step_id || ""));
310
337
  return;
311
338
  default:
312
- console.log(`[otto-bridge] event=${type || "unknown"} payload=${JSON.stringify(message)}`);
339
+ this.logInfo(`[otto-bridge] event=${type || "unknown"} payload=${JSON.stringify(message)}`);
313
340
  }
314
341
  }
315
342
  maybeLogBridgeReleaseNotice(message) {
@@ -328,49 +355,106 @@ export class BridgeRuntime {
328
355
  }
329
356
  this.lastBridgeReleaseNoticeKey = noticeKey;
330
357
  if (updateRequired) {
331
- console.warn(`[otto-bridge] update required current=${this.config.bridgeVersion} min_supported=${minSupportedVersion || "unknown"} latest=${latestVersion || "unknown"} command="${updateCommand}"`);
358
+ const message = `[otto-bridge] update required current=${this.config.bridgeVersion} min_supported=${minSupportedVersion || "unknown"} latest=${latestVersion || "unknown"} command="${updateCommand}"`;
359
+ this.logWarn(message);
360
+ this.emit({ type: "update_required", message });
332
361
  return;
333
362
  }
334
363
  if (updateAvailable) {
335
- console.log(`[otto-bridge] update available current=${this.config.bridgeVersion} latest=${latestVersion || "unknown"} command="${updateCommand}"`);
364
+ const message = `[otto-bridge] update available current=${this.config.bridgeVersion} latest=${latestVersion || "unknown"} command="${updateCommand}"`;
365
+ this.logInfo(message);
366
+ this.emit({ type: "update_available", message });
336
367
  }
337
368
  }
369
+ clearPendingConfirmations(jobId) {
370
+ const normalizedJobId = String(jobId || "").trim();
371
+ if (!normalizedJobId) {
372
+ return;
373
+ }
374
+ for (const key of Array.from(this.pendingConfirmations.keys())) {
375
+ if (key === normalizedJobId || key.startsWith(`${normalizedJobId}:`)) {
376
+ this.pendingConfirmations.delete(key);
377
+ }
378
+ }
379
+ }
380
+ resolvePendingCancellation(jobId, stepId) {
381
+ const normalizedJobId = String(jobId || "").trim();
382
+ if (!normalizedJobId) {
383
+ return false;
384
+ }
385
+ const keys = stepId
386
+ ? [confirmationKey(normalizedJobId, stepId), normalizedJobId]
387
+ : Array.from(this.pendingConfirmations.keys()).filter((key) => (key === normalizedJobId || key.startsWith(`${normalizedJobId}:`)));
388
+ let resolved = false;
389
+ for (const key of keys) {
390
+ const waiter = this.pendingConfirmations.get(key);
391
+ if (!waiter) {
392
+ continue;
393
+ }
394
+ this.pendingConfirmations.delete(key);
395
+ waiter.resolve({
396
+ action: "reject",
397
+ note: "Cancelled by Otto Bridge",
398
+ });
399
+ resolved = true;
400
+ }
401
+ return resolved;
402
+ }
338
403
  resolveConfirmation(message) {
339
404
  const jobId = String(message.job_id || "");
405
+ const stepId = String(message.step_id || "");
340
406
  const action = String(message.action || "").trim().toLowerCase();
341
- const waiter = this.pendingConfirmations.get(jobId);
407
+ const waiter = this.pendingConfirmations.get(confirmationKey(jobId, stepId))
408
+ || this.pendingConfirmations.get(jobId);
342
409
  if (!jobId || !waiter) {
343
- console.warn(`[otto-bridge] unexpected confirmation payload=${JSON.stringify(message)}`);
410
+ this.logWarn(`[otto-bridge] unexpected confirmation payload=${JSON.stringify(message)}`);
344
411
  return;
345
412
  }
346
413
  if (action !== "approve" && action !== "reject") {
347
414
  waiter.reject(new Error(`Unsupported confirmation action: ${action || "unknown"}`));
415
+ this.pendingConfirmations.delete(confirmationKey(jobId, stepId));
348
416
  this.pendingConfirmations.delete(jobId);
349
417
  return;
350
418
  }
419
+ this.pendingConfirmations.delete(confirmationKey(jobId, stepId));
351
420
  this.pendingConfirmations.delete(jobId);
352
421
  waiter.resolve({
353
422
  action,
354
423
  note: typeof message.note === "string" ? message.note : undefined,
355
424
  });
356
425
  }
357
- async waitForConfirmation(jobId) {
426
+ async waitForConfirmation(jobId, stepId) {
358
427
  return await new Promise((resolve, reject) => {
359
- this.pendingConfirmations.set(jobId, { resolve, reject });
428
+ this.pendingConfirmations.set(confirmationKey(jobId, stepId), { resolve, reject });
360
429
  });
361
430
  }
362
- async cancelJob(jobId) {
431
+ async cancelJob(jobId, stepId) {
363
432
  if (!jobId) {
364
433
  return;
365
434
  }
435
+ if (this.resolvePendingCancellation(jobId, stepId)) {
436
+ this.logInfo(`[otto-bridge] confirmation cancelled job=${jobId}${stepId ? ` step=${stepId}` : ""}`);
437
+ return;
438
+ }
366
439
  const cancel = this.activeCancels.get(jobId);
367
440
  if (!cancel) {
368
- console.warn(`[otto-bridge] cancel requested for unknown job=${jobId}`);
441
+ this.logWarn(`[otto-bridge] cancel requested for unknown job=${jobId}`);
369
442
  return;
370
443
  }
444
+ if (stepId) {
445
+ this.logInfo(`[otto-bridge] cancel requested job=${jobId} step=${stepId}`);
446
+ }
371
447
  await cancel();
372
448
  }
373
449
  async executeJob(socket, job) {
450
+ const runtimeManifest = parseJobRuntimeManifest(job);
451
+ const eventStepId = (eventType, options) => {
452
+ const explicitStepId = String(options?.stepId || "").trim();
453
+ if (explicitStepId) {
454
+ return explicitStepId;
455
+ }
456
+ return runtimeStepIdForEvent(runtimeManifest, eventType);
457
+ };
374
458
  const sendJson = async (payload) => {
375
459
  if (socket.readyState !== WebSocket.OPEN) {
376
460
  throw new Error("Socket is not open");
@@ -378,61 +462,78 @@ export class BridgeRuntime {
378
462
  socket.send(JSON.stringify(payload));
379
463
  };
380
464
  this.activeCancels.set(job.job_id, async () => {
381
- this.pendingConfirmations.delete(job.job_id);
465
+ this.clearPendingConfirmations(job.job_id);
382
466
  if (typeof this.executor.cancel === "function") {
383
467
  await this.executor.cancel(job.job_id);
384
468
  }
385
- console.log(`[otto-bridge] job cancelled job_id=${job.job_id}`);
469
+ this.logInfo(`[otto-bridge] job cancelled job_id=${job.job_id}`);
470
+ this.emit({ type: "job_cancelled", jobId: job.job_id });
386
471
  });
387
472
  try {
388
473
  await this.executor.run(job, {
389
- accepted: async () => {
474
+ accepted: async (options) => {
475
+ const stepId = eventStepId("accepted", options);
390
476
  await sendJson({
391
477
  type: "device.job.accepted",
392
478
  device_id: this.config.deviceId,
393
479
  job_id: job.job_id,
480
+ graph_id: runtimeManifest.graphId,
481
+ step_id: stepId,
394
482
  accepted_at: Date.now(),
395
483
  });
396
484
  },
397
- progress: async (progressPercent, progressMessage) => {
485
+ progress: async (progressPercent, progressMessage, options) => {
486
+ const stepId = eventStepId("progress", options);
398
487
  await sendJson({
399
488
  type: "device.job.progress",
400
489
  device_id: this.config.deviceId,
401
490
  job_id: job.job_id,
491
+ graph_id: runtimeManifest.graphId,
492
+ step_id: stepId,
402
493
  progress_percent: progressPercent,
403
494
  progress_message: progressMessage,
404
495
  });
405
496
  },
406
- confirmRequired: async (progressMessage, confirmationContext) => {
407
- const confirmationPromise = this.waitForConfirmation(job.job_id);
497
+ confirmRequired: async (progressMessage, confirmationContext, options) => {
498
+ const stepId = eventStepId("confirm_required", options);
499
+ const confirmationPromise = this.waitForConfirmation(job.job_id, stepId);
408
500
  try {
409
501
  await sendJson({
410
502
  type: "device.job.confirm_required",
411
503
  device_id: this.config.deviceId,
412
504
  job_id: job.job_id,
505
+ graph_id: runtimeManifest.graphId,
506
+ step_id: stepId,
413
507
  progress_message: progressMessage,
414
508
  confirmation_context: confirmationContext || {},
415
509
  });
416
510
  }
417
511
  catch (error) {
512
+ this.pendingConfirmations.delete(confirmationKey(job.job_id, stepId));
418
513
  this.pendingConfirmations.delete(job.job_id);
419
514
  throw error;
420
515
  }
421
516
  return await confirmationPromise;
422
517
  },
423
- completed: async (result) => {
518
+ completed: async (result, options) => {
519
+ const stepId = eventStepId("completed", options);
424
520
  await sendJson({
425
521
  type: "device.job.completed",
426
522
  device_id: this.config.deviceId,
427
523
  job_id: job.job_id,
524
+ graph_id: runtimeManifest.graphId,
525
+ step_id: stepId,
428
526
  result: result || {},
429
527
  });
430
528
  },
431
- failed: async (errorMessage, result) => {
529
+ failed: async (errorMessage, result, options) => {
530
+ const stepId = eventStepId("failed", options);
432
531
  await sendJson({
433
532
  type: "device.job.failed",
434
533
  device_id: this.config.deviceId,
435
534
  job_id: job.job_id,
535
+ graph_id: runtimeManifest.graphId,
536
+ step_id: stepId,
436
537
  error_message: errorMessage,
437
538
  result: result || {},
438
539
  });
@@ -440,16 +541,19 @@ export class BridgeRuntime {
440
541
  });
441
542
  }
442
543
  catch (error) {
443
- this.pendingConfirmations.delete(job.job_id);
544
+ this.clearPendingConfirmations(job.job_id);
444
545
  if (error instanceof JobCancelledError) {
445
546
  return;
446
547
  }
447
548
  const detail = error instanceof Error ? error.message : String(error);
549
+ const stepId = runtimeStepIdForEvent(runtimeManifest, "failed");
448
550
  try {
449
551
  await sendJson({
450
552
  type: "device.job.failed",
451
553
  device_id: this.config.deviceId,
452
554
  job_id: job.job_id,
555
+ graph_id: runtimeManifest.graphId,
556
+ step_id: stepId,
453
557
  error_message: detail || "Executor failed",
454
558
  result: {
455
559
  executor: this.config.executor.type,
@@ -0,0 +1,18 @@
1
+ import { getDeviceJson, postDeviceJson } from "./http.js";
2
+ export async function submitRuntimeCliAssistantPrompt(config, request) {
3
+ return await postDeviceJson(config.apiBaseUrl, config.deviceToken, "/v1/devices/cli/runtime/assistant", request);
4
+ }
5
+ export async function getRuntimeCliJob(config, jobId) {
6
+ return await getDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}`);
7
+ }
8
+ export async function confirmRuntimeCliJob(config, jobId, action, note) {
9
+ return await postDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}/confirm`, {
10
+ action,
11
+ note,
12
+ });
13
+ }
14
+ export async function cancelRuntimeCliJob(config, jobId, note) {
15
+ return await postDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}/cancel`, {
16
+ note,
17
+ });
18
+ }